I have 3 functions like this:
func getMyFirstItem(complete: @escaping (Int) -> Void) {
DispatchQueue.main.async {
complete(10)
}
}
func getMySecondtItem(complete: @escaping (Int) -> Void) {
DispatchQueue.global(qos:.background).async {
complete(10)
}
}
func getMyThirdItem(complete: @escaping (Int) -> Void) {
DispatchQueue.main.async {
complete(10)
}
}
And I have a variable:
var myItemsTotal: Int = 0
I would like to know how to sum the items, in this case 10 10 10 to get 30. But what is the best way, since is background and main.
The key issue is to ensure thread-safety. For example, the following is not thread-safe:
func addUpValuesNotThreadSafe() {
var total = 0
getMyFirstItem { value in
total += value // on main thread
}
getMySecondItem { value in
total += value // on some GCD worker thread!!!
}
getMyThirdItem { value in
total += value // on main thread
}
...
}
One could solve this problem by not allowing these tasks run in parallel, but you lose all the benefits of asynchronous processes and the concurrency they offer.
Needless to say, when you do allow them to run in parallel, you would likely add some mechanism (such as dispatch groups) to know when all of these asynchronous tasks are done. But I did not want to complicate this example, but rather keep our focus on the thread-safety issue. (I show how to use dispatch groups later in this answer.)
Anyway, if you have closures called from multiple threads, you must not increment the same total
without adding some synchronization. You could add synchronization with a serial dispatch queue, for example:
func addUpValues() {
var total = 0
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".synchronized")
getMyFirstItem { value in
queue.async {
total += value // on serial queue
}
}
getMySecondItem { value in
queue.async {
total += value // on serial queue
}
}
getMyThirdItem { value in
queue.async {
total += value // on serial queue
}
}
...
}
There are a variety of alternative synchronization mechanisms (locks, GCD reader-writer, actor
, etc.). But I start with the serial queue example to observe that, actually, any serial queue would accomplish the same thing. Many use the main queue (which is a serial queue) for this sort of trivial synchronization where the performance impact is negligible, such as in this example.
For example, one could therefore either refactor getMySecondItem
to also call its completion handler on the main queue, like getMyFirstItem
and getMyThirdItem
already do. Or if you cannot do that, you could simply have the getMySecondItem
caller dispatch the code that needs to be synchronized to the main queue:
func addUpValues() {
var total = 0
getMyFirstItem { value in
total += value // on main thread
}
getMySecondItem { value in
DispatchQueue.main.async {
total += value // now on main thread, too
}
}
getMyThirdItem { value in
total += value // on main thread
}
// ...
}
That is also thread-safe. This is why many libraries will ensure that all of their completion handlers are called on the main thread, as it minimizes the amount of time the app developer needs to manually synchronize values.
While I have illustrated the use of serial dispatch queues for synchronization, there are a multitude of alternatives. E.g., one might use locks or GCD reader-writer pattern.
The key is that one should never mutate a variable from multiple threads without some synchronization.
Above I mention that you need to know when the three asynchronous tasks are done. You can use a DispatchGroup
, e.g.:
func addUpValues(complete: @escaping (Int) -> Void) {
let total = Synchronized(0)
let group = DispatchGroup()
group.enter()
getMyFirstItem { first in
total.synchronized { value in
value += first
}
group.leave()
}
group.enter()
getMySecondItem { second in
total.synchronized { value in
value += second
}
group.leave()
}
group.enter()
getMyThirdItem { third in
total.synchronized { value in
value += third
}
group.leave()
}
group.notify(queue: .main) {
let value = total.synchronized { $0 }
complete(value)
}
}
And in this example, I abstracted the synchronization details out of addUpValues
:
class Synchronized<T> {
private var value: T
private let lock = NSLock()
init(_ value: T) {
self.value = value
}
func synchronized<U>(block: (inout T) throws -> U) rethrows -> U {
lock.lock()
defer { lock.unlock() }
return try block(&value)
}
}
Obviously, use whatever synchronization mechanism you want (e.g., GCD or os_unfair_lock
or whatever).
But the idea is that in the GCD world, dispatch groups can notify you when a series of asynchronous tasks are done.
I know that this was a GCD question, but for the sake of completeness, the Swift concurrency async
-await
pattern renders much of this moot.
func getMyFirstItem() async -> Int {
return 10
}
func getMySecondItem() async -> Int {
await Task.detached(priority: .background) {
return 10
}.value
}
func getMyThirdItem() async -> Int {
return 10
}
func addUpValues() {
Task {
async let value1 = getMyFirstItem()
async let value2 = getMySecondItem()
async let value3 = getMyThirdItem()
let total = await value1 + value2 + value3
print(total)
}
}
Or, if your async methods were updating some shared property, you would use an actor
to synchronize access. See Protect mutable state with Swift actors.