Freddy Benson Freddy Benson - 1 month ago 17
Swift Question

How to run multiple background thread tasks one at a time? (Swift 3)

First of all, I'm a beginner and I'm about to make a few assumptions of what's causing my problem down here, which may sound really stupid, so please bare with me.

I'm trying to loop through an array of String objects containing dates of the month October 2016, which means 31 String objects: 1 October 2016...31 October 2016. For each object I want to retrieve some data from the database and append the returned value (also a String object) to a new array. The tricky part, though, is that the new array of String objects should be in the exact same order as the dates array. So for example, the 5th object in the new array should be the returned value of the 5th object in my dates array (5 October 2016) and 14th object in the new array should be the returned value of the 14th object in my dates array (14 October 2016) and so on and so forth. Now obviously, the database retrieval process happens in a background thread, and assuming the system wants the entire process to be done as quickly as possible, it fires off multiple retrieval tasks simultaneously (all in their own thread). The problem with this is that it really messes up the order in which the new array is constructed. To be honest, it looks really confusing and the weird part is the order of the array isn't random (which would've probably confirmed my assumption): the order of the new array is basically the first 8 return values in the correct order and then from the 9th value the first 8 values get repeated, so kinda like this:

1 October -> 5
2 October -> 8
3 October -> 4
4 October -> 11
5 October -> 9
6 October -> 7
7 October -> 6
8 October -> 14
9 October -> 5
10 October -> 8
11 October -> 4
12 October -> 11
13 October -> 9
14 October -> 7
15 October -> 6
16 October -> 14
17 October -> 5
18 October -> 8
19 October -> 4
20 October -> 11
21 October -> 9
22 October -> 7
23 October -> 6
24 October -> 14
25 October -> 5
26 October -> 8
27 October -> 4
28 October -> 11
29 October -> 9
30 October -> 7
31 October -> 6


So as you can notice in this pattern, it just fetches 8 different values and then repeats all over again until the array is full. If I run the entire looping process twice in a really short time, the order is usually still the same, except the first value in the array isn't the same anymore (so basically every value moves 1 date up). Anyway, to cut to the chase: I assume running each retrieval task one by one would fix my problem. This is the loop I'm currently running:

// Loop through each date
for date in self.datesToDisplay {
// Fire off retrieval method with date object as its only parameter
self.getMessagesForDate(date)
}


My model will append the returned values to a new array and pass the newly created array back to the caller, like this:

// Delegate method which gets called whenever retrieval is finished
func messagesRetrieved() {
// Pass newly created array back to caller
self.messagesForDatesToDisplay = self.retrieveModel.messages
}


The above code is a little simplified version of the actual code that I'm running in my project, but you get the idea. First question is: am I anywhere near the right direction with my assumption of what could be causing this issue? Second follow-up question is: If I am correct, how can I be sure that the second retrieval process doesn't start until the first one is FULLY COMPLETED (so after the delegate method to return the value has been called and run)?

Rob Rob
Answer

One approach is to retrieve the results into a structure that is not dependent upon the order that the results come in i.e. a dictionary.

So, you could do something like:

let syncQueue = DispatchQueue(label: "...")      // use dispatch_queue_create() in Swift 2

let group = DispatchGroup()                      // use dispatch_group_create() in Swift 2

var results = [String: [Message]]()

for date in datesToDisplay {
    group.enter()                                // use dispatch_group_enter in Swift 2
    getMessages(for: date) { messages in
        syncQueue.async {                        // use dispatch_async in Swift 2
            results[date] = messages
            group.leave()                        // use dispatch_group_leave in Swift 2
        }
    }
}

group.notify(queue: .main) {                     // use dispatch_group_notify in Swift 2
    syncQueue.sync {                             // use dispatch_sync in Swift 2
        // update your model with `results` here
    }
    // trigger UI update here
}

Personally, I'd just stick with the dictionary structure for the results.

For example, if I wanted to get the third entry ("3 October"), it would be

let oct3Messages = modelDictionary[datesToDisplay[2]]

But if you really feel compelled to convert it back to an array of the original order, you can do that:

group.notify(queue: .main) {
    syncQueue.sync {
        self.retrieveModel = self.datesToDisplay.map { results[$0]! }
    }
    // trigger UI update here
}

Now, I made a few subtle changes here. For example, I added a completion handler to getMessagesForDate, so I'd know when the request was done, and I pass the results back in that closure. You want to avoid updating model objects asynchronously and you want to know when everything is done, and I use a dispatch group to coordinate that. I also am using a synchronization queue to coordinate the updates to the results to ensure thread-safety.

But I don't want you to get lost in those details. The key point is that you should simply want to use a structure that is not dependent upon the order that objects are retrieved. Then, either retrieve directly from that unordered dictionary (using your ordered datesToDisplay as the key), or convert it to an ordered array.


In the interest of full disclosure, we should say that it may be more complicated than I suggest here. For example, if your asynchronous getMessagesForDate is just using a global queue, you might want to do something to constrain how many of these run concurrently.

And you might also want to benchmark this against performing these requests sequentially, because while the database may run on a background thread, it might not be able to run multiple requests concurrent with respect to each other (often, the database will synchronize its queries), so you might be going through more work than necessary.