I've just started to learn a little bit more about Grand Central Dispatch in the Swift programming language.
I followed a tutorial online to better understand GCD and tried various examples of usage...
in the section about Work Item I wrote the following code :
func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
workItem.perform()
let queue = DispatchQueue.global(qos: .utility)
queue.async(execute: workItem)
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
}
the code basically perform the workItem in two different queues (the main and global queue) and when the work item finish running in both queues I get the result.
the output of the code above is : 20.
when I tried to manipulate the code a little bit and added another Queue to the mix and ran the same workItem with the same qos
as the global queue (.utility
), like so :
func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
workItem.perform()
let queue = DispatchQueue.global(qos: .utility)
queue.async(execute: workItem)
let que = DispatchQueue(label: "com.appcoda.delayqueue1", qos: .utility)
que.async(execute: workItem)
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
}
the app crashes.
but when I change the order of the commands so I move the workItem.notify
method to the beginning of the method, the app works and give me correct output which is 25 :
func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
workItem.perform()
let queue = DispatchQueue.global(qos: .utility)
queue.async(execute: workItem)
let que = DispatchQueue(label: "com.appcoda.delayqueue1", qos: .utility)
que.async(execute: workItem)
}
can anyone please help understand how the .notify()
method really works ?
and why the order of the commands made a difference ?
thanks a lot in advance...
That first example you share (which I gather is directly from the tutorial) is not well written for a couple of reasons:
It's updating a variable from multiple threads. That is an inherently non thread-safe process. It turns out that for reasons not worth outlining here, it's not technically an issue in the author's original example, but it is a very fragile design, illustrated by the non-thread-safe behavior quickly manifested in your subsequent examples.
One should always synchronize access to a variable if manipulating it from multiple threads. You can use a dedicated serial queue for this, a
NSLock
, reader-writer pattern, or other patterns. While I'd often use another GCD queue for the synchronization, I think that's going to be confusing when we're focusing on the GCD behavior ofDispatchWorkItem
on various queues, so in my example below, I'll useNSLock
to synchronize access, callinglock()
before I try to usevalue
andunlock
when I'm done.You say that first example displays "20". That's a mere accident of timing. If you change it to say ...
... then it will likely say "15", not "20" because you'll see the
notify
for theworkItem.perform()
before theasync
call to the global queue is done. Now, you'd never usesleep
in real apps, but I put it in to illustrate the timing issues.Bottom line, the
notify
on aDispatchWorkItem
happens when the dispatch work item is first completed and it won't wait for the subsequent invocations of it. This code entails what is called a "race condition" between yournotify
block and the call you dispatched to that global queue and you're not assured which will run first.Personally, even setting aside the race conditions and the inherently non thread-safe behavior of mutating some variable from multiple threads, I'd advise against invoking the same
DispatchWorkItem
multiple times, at least in conjunction withnotify
on that work item.If you want to do a notification when everything is done, you should use a
DispatchGroup
, not anotify
on the individualDispatchWorkItem
.Pulling this all together, you get something like: