I am trying to do some calculations on a large number of objects. The objects are saved in an array and the results of the operation should be saved in a new array. To speed up the processing, I‘m trying to break up the task into multiple subtasks which can run concurrently on different threads. The simplified example code below replaces the actual operation with two seconds of wait.
I have tried multiple ways of solving this issue, using both DispatchQueue
s and Task
s.
Using
DispatchQueue
The basic setup I used is the following:
import Foundation class Main { let originalData = ["a", "b", "c"] var calculatedData = Set<String>() func doCalculation() { //calculate length of array slices. let totalLength = originalData.count let sliceLength = Int(totalLength / 3) var start = 0 var end = 0 let myQueue = DispatchQueue(label: "Calculator", attributes: .concurrent) var allPartialResults = [Set<String>]() for i in 0..<3 { if i != 2 { start = sliceLength * i end = start + sliceLength - 1 } else { start = totalLength - sliceLength * (i - 1) end = totalLength - 1 } allPartialResults.append(Set<String>()) myQueue.async { allPartialResults[i] = self.doPartialCalculation(data: Array(self.originalData[start...end])) } } myQueue.sync(flags: .barrier) { for result in allPartialResults { self.calculatedData.formUnion(result) } } //do further calculations with the data } func doPartialCalculation(data: [String]) -> Set<String> { print("began") sleep(2) let someResultSet: Set<String> = ["some result"] print("ended") return someResultSet } }
As expected, the Console Log is the following (with all three "ended" appearing at once, two seconds after all three "began" appeared at once):
began began began ended ended ended
When measuring performance using
os_signpost
(and using real data and calculations), this approach reduces the time needed for the entiredoCalculation()
function to run from 40ms to around 14ms.Note that to avoid data races when appending the results to the final
calculatedData
Set, I created an array of partial Data sets of which every DispatchQueue only accesses one index (which is not a solution I like and the main reason why I am not satisfied with this approach). What I would have liked to do is to callDispatchQueue.main
from within myQueue and add the new data to thecalculatedData
Set on the main thread, however callingDispatchQueue.main.sync
causes a deadlock and using the async version leads to the barrier flag not working as intended.Using Tasks
In a second attempt, I tried using Tasks to run code concurrently. As I understand it, there are two options for running code concurrently with Tasks.
async let
andwithTaskGroup
. For the purpose of retrieving a variable quantity of partial results form a variable amount of concurrent tasks, I figured usingwithTaskGroup
was the best option for me.I modified the code to look like this:
class Main { let originalData = ["a", "b", "c"] var calculatedData = Set<String>() func doCalculation() async { //calculate length of array slices. let totalLength = originalData.count let sliceLength = Int(totalLength / 3) var start = 0 var end = 0 await withTaskGroup(of: Set<String>.self) { group in for i in 0..<3 { if i != 2 { start = sliceLength * i end = start + sliceLength - 1 } else { start = totalLength - sliceLength * (i - 1) end = totalLength - 1 } group.addTask { return await self.doPartialCalculation(data: Array(self.originalData[start...end])) } } for await newSet in group { calculatedData.formUnion(newSet) } } //do further calculations with the data } func doPartialCalculation(data: [String]) async -> Set<String> { print("began") try? await Task.sleep(nanoseconds: UInt64(1e9)) let someResultSet: Set<String> = ["some result"] print("ended") return someResultSet } }
However, the Console Log prints the following (with every "ended" coming 2 seconds after the preceding "before"):
began ended began ended began ended
Measuring performance using
os_signpost
revealed that the operation takes 40ms to complete. Therefore it is not running concurrently.
With that being said, what is the best course of action for this problem?
- Using
DispatchQueue
, how do you call the Main Queue to avoid data races from within a queue, while at the same time preserving a barrier flag later on in the code? - Using
Task
, how do can you actually make them run concurrently?
EDIT
Running the code on a real device instead of the simulator and changing the sleep function inside the Task from sleep()
to Task.sleep()
, I was able to achieve concurrent behavior in that the Console prints the expected log. However, the operation time for the task remains upwards of 40-50ms and is highly variable, sometimes reaching 200ms or more. This problem remains after adding the .userInitiated
property to the Task
.
Why does it take so much longer to run the same operation concurrently using Task
compared to using DispatchQueue
? Am I missing something?