【问题标题】:Why would pthread_mutex_lock() hang if pthread_mutex_unlock() has been called?如果 pthread_mutex_unlock() 已被调用,为什么 pthread_mutex_lock() 会挂起?
【发布时间】:2020-01-30 05:15:59
【问题描述】:

我正在使用 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(&amp;m)
  2. pthread_mutex_unlock(&amp;m)
  3. pthread_mutex_lock(&amp;m)
  4. pthread_mutex_unlock(&amp;m)
  5. pthread_mutex_lock(&amp;m) 僵局

每次运行此代码序列时都会发生这种情况,至少我尝试过 30 多次。两个锁定/解锁序列,然后是死锁。

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

我最好的猜测是这是某种内存问题,即锁以某种方式被释放,但死锁确实发生在我认为拥有锁的对象的 setter 中从记忆的角度来看,所以我不确定情况会如何。

如果有问题的互斥体先前已被锁定然后解锁,pthread_mutex_lock() 调用是否会死锁?

【问题讨论】:

    标签: ios swift pthreads


    【解决方案1】:

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

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

    【讨论】:

    • 我们是否应该在 Synchronized 类的 value 属性的设置器中捕获 self,也许使用 unowned?因为类实例将具有对队列的引用,并且队列正在捕获对自身的引用,从而导致保留周期。为了清楚起见,我说的是使用并发调度队列的版本。
    • 我尝试在操场上运行代码,看起来 Synchronized 类正在被释放,表明没有保留周期。所以我们发送到调度队列的块捕获的自引用在块执行后被释放stackoverflow.com/questions/21987067/…
    • 不存在强引用循环,因此不需要weak(或unowned)。
    【解决方案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)
        }
    }
    

    【讨论】:

    • 谢谢!我按照你的建议做了,僵局还没有再次发生。为了安全起见,我还按照@Rob 的建议在访问器中放置了锁定/解锁调用。
    猜你喜欢
    • 2018-03-27
    • 1970-01-01
    • 2018-10-29
    • 2023-04-05
    • 2012-09-10
    • 1970-01-01
    • 2011-08-05
    • 1970-01-01
    • 2011-08-13
    相关资源
    最近更新 更多