Как избежать гонки данных с GCD DispatchWorkItem.notify?
В Swift 3.1 на 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
упоминает, что это позволяет:
получать уведомления об их завершении
что подразумевает, что notify
обратный вызов вызывается после выполнения рабочего элемента.
Так что я ожидаю, что будет happens-before
отношения между DispatchWorkItem
закрытие работы и ее закрытие. Каков был бы правильный способ, если таковые имеются, использовать DispatchWorkItem
с зарегистрированным notify
обратный вызов, подобный этому, который не вызовет ошибку Thread Sanitizer?
Я пытался зарегистрировать notify
с item.notify(flags: .barrier, queue: .main) ...
но гонка сохранялась (возможно, потому что флаг применяется только к той же очереди, документации о том, что .barrier
флаг делает). Но даже вызов уведомления в той же (фоновой) очереди, что и выполнение рабочего элемента, с flags: .barrier
, результаты в гонке.
Если вы хотите попробовать это, я опубликовал полный проект XCode на github здесь: https://github.com/mna/TestDispatchNotify
Есть TestDispatchNotify
схема, которая создает приложение без цана, и TestDispatchNotify+Tsan
с активированным дезинфицирующим средством.
Спасибо мартин
1 ответ
Ну, похоже, я узнал. Используя 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
!!! WTF!!! Вы все еще думаете, что решили гонку в своем коде? Для синхронизации любого чтения, записи... используйте последовательную очередь, барьер или семафор. Группа рассылки - это совершенно другой зверь:-) С группами рассылки вы можете сгруппировать несколько задач и либо дождаться их завершения, либо получить уведомление после их завершения.
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