我刚刚开始学习用Swift编程语言编写的GrandCentralDispatch。
为了更好地理解GCD,我在网上学习了一个教程,并尝试了各种用法示例…
在关于工作项的部分中,我编写了以下代码:
func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
workItem.perform()
let queue = DispatchQueue.global(qos: .utility)
queue.async(execute: workItem)
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
}
代码基本上在两个不同的队列(主队列和全局队列)中执行工作项,当工作项在两个队列中运行完毕时,我会得到结果。
以上代码的输出为:20。
当我尝试稍微操纵代码,并向混合中添加另一个队列,并使用与全局队列相同的
qos
运行相同的工作项时,如: func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
workItem.perform()
let queue = DispatchQueue.global(qos: .utility)
queue.async(execute: workItem)
let que = DispatchQueue(label: "com.appcoda.delayqueue1", qos: .utility)
que.async(execute: workItem)
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
}
应用程序崩溃。
但是,当我更改命令的顺序以便将
.utility
方法移动到方法的开头时,应用程序会工作,并为我提供正确的输出,即25:func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
workItem.perform()
let queue = DispatchQueue.global(qos: .utility)
queue.async(execute: workItem)
let que = DispatchQueue(label: "com.appcoda.delayqueue1", qos: .utility)
que.async(execute: workItem)
}
有人能帮助理解
workItem.notify
方法是如何真正起作用的吗?为什么命令的顺序会有所不同呢?
提前多谢了…
最佳答案
您共享的第一个示例(我从教程中直接收集到)编写得不好,原因有两个:
它正在从多个线程更新一个变量。这是一个固有的非线程安全过程。事实证明,出于不值得在这里概述的原因,在作者最初的示例中,这在技术上不是一个问题,但它是一个非常脆弱的设计,通过在随后的示例中快速显示的非线程安全行为可以说明这一点。
如果从多个线程操作变量,则应该始终访问该变量。您可以为此使用专用的串行队列、读写器模式或其他模式。虽然我经常使用另一个GCD队列进行同步,但当我们关注不同队列上NSLock
的GCD行为时,我认为这会令人困惑,因此在下面的示例中,我将使用DispatchWorkItem
来同步访问,在尝试使用NSLock
和lock()
之前调用value
。
你说第一个例子显示“20”。那只是时间上的意外。如果你把它改成…
let workItem = DispatchWorkItem {
os_log("starting")
Thread.sleep(forTimeInterval: 2)
value += 5
os_log("done")
}
…然后它可能会说“15”,而不是“20”,因为在对全局队列的调用完成之前,您将看到
unlock
的notify
。现在,你永远不会在真正的应用程序中使用workItem.perform()
,但我把它放在里面来说明时间问题。底线是,
async
上的sleep
发生在调度工作项第一次完成时,它不会等待随后的调用。此代码包含notify
块与调度到该全局队列的调用之间所谓的“争用条件”,您不确定哪个将首先运行。就个人而言,即使不考虑竞争条件和从多个线程改变某个变量的固有非线程安全行为,我建议不要多次调用相同的
DispatchWorkItem
,至少要与该工作项上的notify
结合使用。如果您希望在完成所有操作后发出通知,则应在单个
DispatchWorkItem
上使用notify
,而不是DispatchGroup
。把这些放在一起,你会发现:
import os.log
var value = 10
let lock = NSLock() // a lock to synchronize our access to `value`
func notifyExperiment() {
// rather than using `DispatchWorkItem`, a reference type, and invoking it multiple times,
// let's just define some closure or function to run some task
func performTask(message: String) {
os_log("starting %@", message)
Thread.sleep(forTimeInterval: 2) // we wouldn't do this in production app, but lets do it here for pedagogic purposes, slowing it down enough so we can see what's going on
lock.lock()
value += 5
lock.unlock()
os_log("done %@", message)
}
// create a dispatch group to keep track of when these tasks are done
let group = DispatchGroup()
// let's enter the group so that we don't have race condition between dispatching tasks
// to the queues and our notify process
group.enter()
// define what notification will be done when the task is done
group.notify(queue: .main) {
self.lock.lock()
os_log("value = %d", self.value)
self.lock.unlock()
}
// Let's run our task once on the global queue
DispatchQueue.global(qos: .utility).async(group: group) {
performTask(message: "from global queue")
}
// Let's run our task also on a custom queue
let customQueue = DispatchQueue(label: "com.appcoda.delayqueue1", qos: .utility)
customQueue.async(group: group) {
performTask(message: "from custom queue")
}
// Now let's leave the group, resolving our `enter` at the top, allowing the `notify` block
// to run iff (a) all `enter` calls are balanced with `leave` calls; and (b) once the `async(group:)`
// calls are done.
group.leave()
}
关于swift - 为什么DispatchWorkItem通知崩溃?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/47944786/