我正在将Swift库用于可变的可观察对象,该库使用互斥量来控制对可观察值的读取/写入,如下所示:
import Foundation
class MutObservable<T> {
lazy var m: pthread_mutex_t = {
var m = pthread_mutex_t()
pthread_mutex_init(&m, nil)
return m
}()
var value: T {
get {
return _value
}
set {
pthread_mutex_lock(&m)
_value = newValue
pthread_mutex_unlock(&m)
}
}
}
此代码遇到死锁。通过插入断点并逐步执行,我观察到以下几点:
pthread_mutex_lock(&m)
pthread_mutex_unlock(&m)
pthread_mutex_lock(&m)
pthread_mutex_unlock(&m)
pthread_mutex_lock(&m)
死锁 每次运行此代码序列时都会发生这种情况,至少我尝试了30次以上。两个锁定/解锁序列,然后是死锁。
根据我在其他语言中使用互斥锁的经验,(转到)我不希望相等的锁定/解锁调用产生死锁,但是我认为这是一个直接的C互斥锁,因此这里可能存在一些规则,我不是熟悉。可能还会有Swift / C互操作因素。
我的最佳猜测是这是某种内存问题,例如,锁以某种方式被释放,但是死锁实际上是在对象的setter中发生的,我相信该对象拥有该锁从内存的角度来看,所以我不确定情况如何。
如果所讨论的互斥锁先前已被锁定然后再解锁,那么pthread_mutex_lock()
调用会死锁是有原因的吗?
答案 0 :(得分:2)
问题是您使用的是本地(例如堆栈)变量作为互斥量。这绝不是一个好主意,因为堆栈高度可变且难以预测。
此外,使用lazy
也不是很好,因为可能返回不同的地址(根据我的测试)。因此,我建议使用init
来初始化互斥锁:
class MutObservable<T> {
private var m = pthread_mutex_t()
var _value:T
var value: T {
get {
return _value
}
set {
pthread_mutex_lock(&m)
setCount += 1
_value = newValue
pthread_mutex_unlock(&m)
}
}
init(v:T) {
_value = v
pthread_mutex_init(&m, nil)
}
}
答案 1 :(得分:2)
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 }
}
现在,在整个操作同步的情况下,我们得到正确的结果。这是一个简单的示例,但是它说明了为什么即使在像上面这样的简单示例中,在访问器中埋藏同步也常常不够。