Почему 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 ответ
Первый пример, которым вы поделились (который я собираю, прямо из учебника), не очень хорошо написан по нескольким причинам:
Это обновление переменной из нескольких потоков. Это по сути не потокобезопасный процесс. Оказывается, что по причинам, которые здесь не стоит обрисовывать, технически это не является проблемой в оригинальном примере автора, но это очень хрупкая конструкция, проиллюстрированная небезопасным поведением, быстро проявляющимся в ваших последующих примерах.
Нужно всегда синхронизировать доступ к переменной, если манипулируешь ею из нескольких потоков. Вы можете использовать выделенную последовательную очередь для этого,
NSLock
, читатель-писатель шаблон или другие шаблоны. Хотя я часто использую другую очередь GCD для синхронизации, я думаю, что это будет сбивать с толку, когда мы сосредоточимся на поведении GCDDispatchWorkItem
в различных очередях, поэтому в моем примере ниже я буду использоватьNSLock
синхронизировать доступ, звоняlock()
прежде чем я попытаюсь использоватьvalue
а такжеunlock
когда я уже закончил.Вы говорите, что в первом примере отображается "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 и вызов, который вы отправили в эту глобальную очередь, и вы не уверены, что будет выполнено первым.Лично, даже если оставить в стороне условия гонки и неотъемлемо безопасное для потоков поведение мутации некоторой переменной из нескольких потоков, я бы посоветовал не вызывать одно и то же.
DispatchWorkItem
несколько раз, по крайней мере, в сочетании сnotify
на этом рабочем месте.Если вы хотите сделать уведомление, когда все сделано, вы должны использовать
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()
}