如果 pthread_mutex_unlock() 已被调用,为什么 pthread_mutex_lock() 会挂起?
Why would pthread_mutex_lock() hang if pthread_mutex_unlock() has been called?
我正在为可变可观察对象使用 Swift 库,该库使用互斥体来控制可观察值的 reads/writes,如下所示:
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 多次。两个 lock/unlock 序列,然后是死锁。
根据我在其他语言中使用互斥锁的经验,(Go) 我不希望相等 lock/unlock 调用产生死锁,但我认为这是一个直接的 C 互斥锁,所以可能有规则这里我不熟悉。混合中还可能存在 Swift/C 互操作因素。
我最好的猜测是这是某种内存问题,即锁正在以某种方式解除分配,但死锁实际上是 在 setter 中发生的从内存的角度来看,我认为拥有锁的对象,所以我不确定情况会怎样。
如果有问题的互斥锁之前被锁定然后解锁,pthread_mutex_lock()
调用会死锁的原因是什么?
问题是您正在使用本地(例如堆栈)变量作为互斥量。这绝不是一个好主意,因为堆栈是高度可变且不可预测的。
此外,使用 lazy
效果不佳,因为可能 return 不同的地址(根据我的测试)。因此我建议使用 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)
}
}
FWIW,在 WWDC 2016 视频 Concurrent Programming with GCD 中,他们指出虽然您过去可能使用过 pthread_mutex_t
,但他们现在不鼓励我们使用它。他们展示了如何使用传统锁(推荐 os_unfair_lock
作为一种性能更高的解决方案,但不会遇到旧的已弃用自旋锁的电源问题),但如果你想这样做,他们建议您派生了一个 Objective-C 基础 class 和基于结构的锁作为 ivars。但是他们警告我们,我们不能直接从 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 队列来实现。串行队列是最简单的解决方案,但您也可以在并发队列上使用 reader-writer 模式,reader 使用 sync
,但 writer 使用 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
}
这几乎肯定不会 return 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 { [=15=] += 1 }
}
现在,随着整个操作的同步,我们得到了正确的结果。这是一个简单的示例,但它说明了为什么将同步隐藏在访问器中常常是不够的,即使在像上面这样的简单示例中也是如此。
我正在为可变可观察对象使用 Swift 库,该库使用互斥体来控制可观察值的 reads/writes,如下所示:
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 多次。两个 lock/unlock 序列,然后是死锁。
根据我在其他语言中使用互斥锁的经验,(Go) 我不希望相等 lock/unlock 调用产生死锁,但我认为这是一个直接的 C 互斥锁,所以可能有规则这里我不熟悉。混合中还可能存在 Swift/C 互操作因素。
我最好的猜测是这是某种内存问题,即锁正在以某种方式解除分配,但死锁实际上是 在 setter 中发生的从内存的角度来看,我认为拥有锁的对象,所以我不确定情况会怎样。
如果有问题的互斥锁之前被锁定然后解锁,pthread_mutex_lock()
调用会死锁的原因是什么?
问题是您正在使用本地(例如堆栈)变量作为互斥量。这绝不是一个好主意,因为堆栈是高度可变且不可预测的。
此外,使用 lazy
效果不佳,因为可能 return 不同的地址(根据我的测试)。因此我建议使用 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)
}
}
FWIW,在 WWDC 2016 视频 Concurrent Programming with GCD 中,他们指出虽然您过去可能使用过 pthread_mutex_t
,但他们现在不鼓励我们使用它。他们展示了如何使用传统锁(推荐 os_unfair_lock
作为一种性能更高的解决方案,但不会遇到旧的已弃用自旋锁的电源问题),但如果你想这样做,他们建议您派生了一个 Objective-C 基础 class 和基于结构的锁作为 ivars。但是他们警告我们,我们不能直接从 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 队列来实现。串行队列是最简单的解决方案,但您也可以在并发队列上使用 reader-writer 模式,reader 使用 sync
,但 writer 使用 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
}
这几乎肯定不会 return 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 { [=15=] += 1 }
}
现在,随着整个操作的同步,我们得到了正确的结果。这是一个简单的示例,但它说明了为什么将同步隐藏在访问器中常常是不够的,即使在像上面这样的简单示例中也是如此。