如何避免与 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
使用 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