为什么 DispatchWorkItem 会通知 crash?

Why does DispatchWorkItem notify crash?

我刚刚开始更多地了解 Swift 编程语言中的 G运行d Central Dispatch。

我按照在线教程更好地理解 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 的相同工作项 (.utility) ,像这样:

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

应用程序崩溃。

但是当我改变命令的顺序时,我将 workItem.notify 方法移动到方法的开头,应用程序工作并给我正确的输出,即 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)
}

任何人都可以帮助理解 .notify() 方法是如何工作的吗? 为什么命令的顺序会有所不同?

非常感谢...

你分享的第一个例子(我直接从教程中收集的)写得不好有几个原因:

  1. 它正在从多个线程更新一个变量。这是一个本质上非线程安全的过程。事实证明,出于不值得在此概述的原因,这在作者的原始示例中在技术上不是问题,但它是一个非常脆弱的设计,在您的后续示例中迅速体现的非线程安全行为说明了这一点。

    如果从多个线程操作变量,则应始终 synchronize 访问变量。您可以为此使用专用的串行队列、NSLock、reader-writer 模式或其他模式。虽然我经常使用另一个 GCD 队列进行同步,但我认为当我们关注 DispatchWorkItem 在各种队列上的 GCD 行为时,这会造成混淆,因此在下面的示例中,我将使用 NSLock 同步访问,在我尝试使用 value 之前调用 lock() 并在完成后调用 unlock

  2. 你说第一个例子显示“20”。这只是时间上的意外。如果你把它改成...

    let workItem = DispatchWorkItem {
        os_log("starting")
        Thread.sleep(forTimeInterval: 2)
        value += 5
        os_log("done")
    }
    

    ... 那么它可能会说“15”,而不是“20”,因为在调用全局 async 之前,您会看到 workItem.perform()notify队列完成。现在,您永远不会在实际应用程序中使用 sleep,但我将其放入以说明时间问题。

    最重要的是,DispatchWorkItem 上的 notify 发生在调度工作项首次完成时,它不会等待后续调用。此代码需要在您的 notify 块和您分配给该全局队列的调用之间进行所谓的 "race condition",您不确定哪个会先 运行。

  3. 就个人而言,即使抛开竞争条件和从多个线程改变某些变量的固有非线程安全行为,我建议不要多次调用相同的 DispatchWorkItem,至少与该工作项目上的 notify 结合使用。

  4. 如果你想在一切完成后做一个通知,你应该使用 DispatchGroup,而不是 DispatchWorkItem 上的 notify

把所有这些放在一起,你会得到类似的东西:

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