FWIW,在 WWDC 2016 视频 Concurrent Programming with GCD 中指出,虽然您过去可能使用过 pthread_mutex_t,但他们现在不鼓励我们使用它。他们展示了如何使用传统锁(推荐os_unfair_lock 作为一种性能更高的解决方案,但不会遇到旧的不推荐使用的自旋锁的电源问题),但如果你想这样做,他们建议你派生具有基于结构的锁作为 ivars 的 Objective-C 基类。但他们警告我们,我们不能直接从 Swift 安全地使用旧的基于 C 结构的锁。
但是不再需要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
}
}
我建议针对您的用例对各种替代方案进行基准测试,看看哪种方案最适合您。
请注意,我正在同步读取和写入。仅对写入使用同步将防止同时写入,但不能防止同时读取和写入(读取可能因此产生无效结果)。
确保与底层对象同步所有交互。
所有这些都已经说过,在访问器级别执行此操作(就像您所做的那样,就像我在上面展示的那样)几乎总是不足以实现线程安全。同步必须始终处于更高的抽象级别。考虑这个简单的例子:
let counter = Synchronized(value: 0)
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
counter.value += 1
}
这几乎肯定不会返回 1,000,000。这是因为同步处于错误的级别。请参阅 Swift Tip: Atomic Variables 讨论问题所在。
您可以通过添加 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 }
}
现在,随着整个操作同步,我们得到了正确的结果。这是一个简单的示例,但它说明了为什么将同步隐藏在访问器中通常是不够的,即使在像上面这样的简单示例中也是如此。