如果调用了pthread_mutex_unlock(),为什么pthread_mutex_lock()会挂起?

时间:2020-01-30 05:15:59

标签: ios swift pthreads

我正在将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)
        }
    }

}

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

  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次以上。两个锁定/解锁序列,然后是死锁。

根据我在其他语言中使用互斥锁的经验,(转到)我不希望相等的锁定/解锁调用产生死锁,但是我认为这是一个直接的C互斥锁,因此这里可能存在一些规则,我不是熟悉。可能还会有Swift / C互操作因素。

我的最佳猜测是这是某种内存问题,例如,锁以某种方式被释放,但是死锁实际上是在对象的setter中发生的,我相信该对象拥有该锁从内存的角度来看,所以我不确定情况如何。

如果所讨论的互斥锁先前已被锁定然后再解锁,那么pthread_mutex_lock()调用会死锁是有原因的吗?

2 个答案:

答案 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 }
}

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