With Swift 3.1 on XCode 8.3, running the following code with the Thread Sanitizer finds a data race (see the write and read comments in the code):
private func incrementAsync() {
let item = DispatchWorkItem { [weak self] in
guard let strongSelf = self else { return }
strongSelf.x += 1 // <--- the write
// Uncomment following line and there's no race, probably because print introduces a barrier
//print("> DispatchWorkItem done")
}
item.notify(queue: .main) { [weak self] in
guard let strongSelf = self else { return }
print("> \(strongSelf.x)") // <--- the read
}
DispatchQueue.global(qos: .background).async(execute: item)
}
This seems pretty strange to me as the documentation for the DispatchWorkItem
mentions that it allows:
getting notified about their completion
which implies that the notify
callback is called once the work item's execution is done.
So I would expect that there would be a happens-before
relationship between the DispatchWorkItem
's work closure and its notify closure. What would be the correct way, if any, to use a DispatchWorkItem
with a registered notify
callback like this that wouldn't trigger the Thread Sanitizer error?
I tried registering the notify
with item.notify(flags: .barrier, queue: .main) ...
but the race persisted (probably because the flag only applies to the same queue, documentation is sparse on what the .barrier
flag does). But even calling notify on the same (background) queue as the work item's execution, with the flags: .barrier
, results in a race.
If you wanna try this out, I published the complete XCode project on github here: https://github.com/mna/TestDispatchNotify
There's a TestDispatchNotify
scheme that builds the app without tsan, and TestDispatchNotify+Tsan
with the Thread Sanitizer activated.
Thanks, Martin
EDIT (2019-01-07): As mentioned by @Rob in a comment on the question, this can't be reproduced anymore with recent versions of Xcode/Foundation (I don't have Xcode installed anymore, I won't guess a version number). There is no workaround required.
Well looks like I found out. Using a DispatchGroup.notify
to get notified when the group's dispatched items have completed, instead of DispatchWorkItem.notify
, avoids the data race. Here's the same-ish snippet without the data race:
private func incrementAsync() {
let queue = DispatchQueue.global(qos: .background)
let item = DispatchWorkItem { [weak self] in
guard let strongSelf = self else { return }
strongSelf.x += 1
}
let group = DispatchGroup()
group.notify(queue: .main) { [weak self] in
guard let strongSelf = self else { return }
print("> \(strongSelf.x)")
}
queue.async(group: group, execute: item)
}
So DispatchGroup
introduces a happens-before relationship and notify
is safely called after the threads (in this case, a single async work item) finished execution, while DispatchWorkItem.notify
doesn't offer this guarantee.