Search code examples
swiftgrand-central-dispatchthread-sanitizer

How to avoid data race with GCD DispatchWorkItem.notify?


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


Solution

  • 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.