如果 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)
        }
    }

}

我遇到此代码的死锁。通过插入断点并单步执行,我观察到以下内容:

  1. pthread_mutex_lock(&m)
  2. pthread_mutex_unlock(&m)
  3. pthread_mutex_lock(&m)
  4. pthread_mutex_unlock(&m)
  5. 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 }
}

现在,随着整个操作的同步,我们得到了正确的结果。这是一个简单的示例,但它说明了为什么将同步隐藏在访问器中常常是不够的,即使在像上面这样的简单示例中也是如此。