如何使用GCD DispatchWorkItem.notify避免数据竞争?

时间:2017-03-31 01:49:23

标签: swift grand-central-dispatch thread-sanitizer

在XCode 8.3上使用Swift 3.1,使用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的文档提到它允许:

  

收到有关完成情况的通知

表示在完成工作项的执行后调用notify回调。

所以我希望happens-before的工作结束与其通知结束之间存在DispatchWorkItem关系。使用带有注册DispatchWorkItem回调的notify以及不会触发Thread Sanitizer错误的正确方法(如果有的话)是什么?

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

如果您想尝试一下,我在github上发布了完整的XCode项目:https://github.com/mna/TestDispatchNotify

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

谢谢, 马丁

2 个答案:

答案 0 :(得分:1)

编辑(2019-01-07):正如@Rob在对该问题的评论中提到的那样,不能再使用最新版本的Xcode / Foundation(我不喜欢#39;已经安装了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没有&#39} ;提供这种保证。

答案 1 :(得分:0)

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

即使作业永远不会执行

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

让我们使用DispatchGroup尝试这个示例!

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true


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

并查看结果

group notify

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

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