FWIW, в WWD C 2016 видео Параллельное программирование с GCD они указывают, что, хотя вы могли исторически использовать pthread_mutex_t
, они теперь отговаривают нас от его использования. Они показывают, как вы можете использовать традиционные блокировки (рекомендует os_unfair_lock
в качестве более производительного решения, но не испытывающие проблем с питанием со старой устаревшей спин-блокировкой), но если вы хотите сделать это, они советуют вам извлечь базовый класс Objective- C с блокировками на основе структуры в виде ivars. Но они предупреждают нас, что мы просто не можем просто безопасно использовать старые блокировки на основе структуры C непосредственно из Swift.
Но в блокировках pthread_mutex_t
больше нет необходимости. Лично я нахожу, что простое NSLock
является довольно производительным решением, поэтому я лично использую расширение (на основе паттерна Apple, использованного в их примере «расширенных операций»):
extension NSLocking {
func synchronized<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try closure()
}
}
Тогда я могу определить заблокируйте и используйте этот метод:
class Synchronized<T> {
private var _value: T
private var lock = NSLock()
var value: T {
get { lock.synchronized { _value } }
set { lock.synchronized { _value = newValue } }
}
init(value: T) {
_value = value
}
}
Это видео (посвященное GCD) показывает, как вы можете сделать это с очередями GCD. Последовательная очередь - самое простое решение, но вы можете также использовать шаблон чтения-записи в параллельной очереди, при этом читатель использует sync
, а писатель использует async
с барьером:
class Synchronized<T> {
private var _value: T
private var queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".synchronizer", attributes: .concurrent)
var value: T {
get { queue.sync { _value } }
set { queue.async(flags: .barrier) { self._value = newValue } }
}
init(value: T) {
_value = value
}
}
I рекомендую сравнить различные альтернативы для вашего варианта использования и посмотреть, какой из них лучше для вас.
Обратите внимание, что я синхронизирую как чтение, так и запись. Только использование синхронизации при записи защитит от одновременных операций записи, но не от одновременных операций чтения и записи (где чтение может, следовательно, привести к неверному результату).
Обязательно синхронизируйте все взаимодействия с базовым объектом.
Все это было сказано, делая это на уровне доступа (как вы сделали, так и как я) как показано выше) почти всегда недостаточно для обеспечения безопасности потока. Неизменно синхронизация должна быть на более высоком уровне абстракции. Рассмотрим этот тривиальный пример:
let counter = Synchronized(value: 0)
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
counter.value += 1
}
Это почти наверняка не вернет 1 000 000. Это потому, что синхронизация на неправильном уровне. См. Swift Совет: Atomi c Переменные для обсуждения того, что не так.
Вы можете исправить это, добавив метод synchronized
, чтобы обернуть все, что требует синхронизации (в данном случае, извлечение значения, его приращение и сохранение результата):
class Synchronized<T> {
private var _value: T
private var lock = NSLock()
var value: T {
get { lock.synchronized { _value } }
set { lock.synchronized { _value = newValue } }
}
func synchronized(block: (inout T) throws -> Void) rethrows {
try lock.synchronized { try block(&_value) }
}
init(value: T) {
_value = value
}
}
И затем:
let counter = Synchronized(value: 0)
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
counter.synchronized { $0 += 1 }
}
Теперь, когда вся операция синхронизирована, мы получаем правильный результат. Это тривиальный пример, но он показывает, почему скрыть синхронизацию в средствах доступа часто недостаточно, даже в тривиальном примере, подобном приведенному выше.