Почему DispatchWorkItem уведомляет о сбое?

Я только начал немного больше узнавать о Grand Central Dispatch на языке программирования Swift.

Я следовал учебному пособию онлайн, чтобы лучше понять 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)
    }
}

код в основном выполняет work Item в двух разных очередях (основной и глобальной очереди), и когда рабочий элемент завершает работу в обеих очередях, я получаю результат.

вывод кода выше: 20.

когда я попытался немного манипулировать кодом, добавил еще одну очередь к миксу и запустил тот же самый Work Item с тем же 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 ответ

Решение

Первый пример, которым вы поделились (который я собираю, прямо из учебника), не очень хорошо написан по нескольким причинам:

  1. Это обновление переменной из нескольких потоков. Это по сути не потокобезопасный процесс. Оказывается, что по причинам, которые здесь не стоит обрисовывать, технически это не является проблемой в оригинальном примере автора, но это очень хрупкая конструкция, проиллюстрированная небезопасным поведением, быстро проявляющимся в ваших последующих примерах.

    Нужно всегда синхронизировать доступ к переменной, если манипулируешь ею из нескольких потоков. Вы можете использовать выделенную последовательную очередь для этого, NSLock, читатель-писатель шаблон или другие шаблоны. Хотя я часто использую другую очередь GCD для синхронизации, я думаю, что это будет сбивать с толку, когда мы сосредоточимся на поведении GCD DispatchWorkItem в различных очередях, поэтому в моем примере ниже я буду использовать NSLock синхронизировать доступ, звоня lock() прежде чем я попытаюсь использовать value а также unlock когда я уже закончил.

  2. Вы говорите, что в первом примере отображается "20". Это просто случайность времени. Если вы измените это, чтобы сказать...

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

    ... тогда он, скорее всего, скажет "15", а не "20", потому что вы увидите notify для workItem.perform() перед async вызов в глобальную очередь завершен. Теперь вы никогда не будете использовать sleep в реальных приложениях, но я вставил это, чтобы проиллюстрировать проблемы синхронизации.

    Итог, notify на DispatchWorkItem происходит, когда рабочий элемент диспетчеризации впервые завершен, и он не будет ожидать последующих его вызовов. Этот код влечет за собой то, что называется "состояние гонки" между вашими notify block и вызов, который вы отправили в эту глобальную очередь, и вы не уверены, что будет выполнено первым.

  3. Лично, даже если оставить в стороне условия гонки и неотъемлемо безопасное для потоков поведение мутации некоторой переменной из нескольких потоков, я бы посоветовал не вызывать одно и то же. DispatchWorkItem несколько раз, по крайней мере, в сочетании с notify на этом рабочем месте.

  4. Если вы хотите сделать уведомление, когда все сделано, вы должны использовать DispatchGroupне notify на человека DispatchWorkItem,

Собрав все это вместе, вы получите что-то вроде:

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()
}
Другие вопросы по тегам