如何避免与 GCD DispatchWorkItem.notify 的数据竞争?

How to avoid data race with GCD DispatchWorkItem.notify?

使用 Swift 3.1 on XCode 8.3,运行 以下代码与 Thread Sanitizer 发现数据竞争(请参阅代码中的写入和读取注释):

  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)
  }

这对我来说似乎很奇怪,因为 DispatchWorkItem 的文档提到它允许:

getting notified about their completion

这意味着一旦工作项的执行完成,就会调用 notify 回调。

所以我希望 DispatchWorkItem 的工作闭包和它的通知闭包之间存在 happens-before 关系。如果有的话,将 DispatchWorkItem 与注册的 notify 回调一起使用而不会触发 Thread Sanitizer 错误的正确方法是什么?

我尝试用 item.notify(flags: .barrier, queue: .main) ... 注册 notify 但竞争仍然存在(可能是因为该标志仅适用于同一队列,关于 .barrier 标志的作用的文档很少) .但即使在与工作项执行相同的(后台)队列上调用通知,flags: .barrier,也会导致竞争。

如果你想试试这个,我在 github 上发布了完整的 XCode 项目:https://github.com/mna/TestDispatchNotify

有一个 TestDispatchNotify 方案可以在没有 tsan 的情况下构建应用程序,并且 TestDispatchNotify+Tsan 可以激活 Thread Sanitizer。

谢谢, 马丁

EDIT (2019-01-07): 正如@Rob 在对该问题的评论中提到的,[=25 的最新版本无法再重现=](我没有安装 Xcode,我不会猜测版本号)。不需要解决方法。


看来我发现了。使用 DispatchGroup.notify 代替 DispatchWorkItem.notify 来在组的派发项目完成时获得通知,避免了数据竞争。这是没有数据竞争的相同片段:

  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)
  }

因此 DispatchGroup 引入了先行关系,并且 notify 在线程(在本例中为单个异步工作项)完成执行后被安全调用,而 DispatchWorkItem.notify 则不会'提供此保证。

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

var job = DispatchWorkItem {
    for i in 0..<3 {
        DispatchQueue.main.async {
            print("job", i)
        }
    }
    DispatchQueue.main.async {
        print("job done")
    }
}
job.notify(queue: .main) {
    print("job notify")
}

DispatchQueue.global(qos: .background).asyncAfter(deadline: .now(), execute: job)
usleep(100)
job.cancel()

如果您猜到这个片段打印出来了

job 0
job 1
job 2
job done
job notify

你完全正确! 增加截止日期...

DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.01, execute: job)

你有

job notify

即使作业从不执行

notify 与 DispatchWorkItem 的闭包捕获的任何数据的同步无关。

让我们用 DispatchGroup 试试这个例子!

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true


let group = DispatchGroup()
group.notify(queue: .main) {
    print("group notify")
}

并查看结果

group notify

!!!卧槽!!!你还认为你在代码中解决了比赛吗? 要同步任何读取、写入...使用串行队列、屏障或信号量。调度组是完全不同的野兽 :-) 使用调度组,您可以将多个任务组合在一起,然后等待它们完成或在它们完成后收到通知。

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

let job1 = DispatchWorkItem {
    sleep(1)
    DispatchQueue.main.async {
        print("job 1 done")
    }
}
let job2 = DispatchWorkItem {
    sleep(2)
    DispatchQueue.main.async {
        print("job 2 done")
    }
}
let group = DispatchGroup()
DispatchQueue.global(qos: .background).async(group: group, execute: job1)
DispatchQueue.global(qos: .background).async(group: group, execute: job2)

print("line1")
group.notify(queue: .main) {
    print("group notify")
}
print("line2")

打印

line1
line2
job 1 done
job 2 done
group notify