Как избежать гонки данных с 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
Другие вопросы по тегам