如何借助信号量/锁解决数据竞争/读写问题?

Posted

技术标签:

【中文标题】如何借助信号量/锁解决数据竞争/读写问题?【英文标题】:How to solve data race/ read and write problem with a help of semaphore/lock? 【发布时间】:2021-10-08 09:29:54 【问题描述】:

是否可以借助信号量或锁来解决读写问题? 可以使解决方案具有串行写入和串行读取,但是否可以进行并发读取(提供一次并发读取的可能性)?

这是我的简单实现,但读取不是并发的。

class ThreadSafeContainerSemaphore<T> 
    private var value: T
    private let semaphore = DispatchSemaphore(value: 1)
    
    func get() -> T 
        semaphore.wait()
        defer  semaphore.signal() 
        return value
    
    
    func mutate(_ completion: (inout T) -> Void) 
        semaphore.wait()
        completion(&self.value)
        semaphore.signal()
    

    init(value: T) 
        self.value = value
    

【问题讨论】:

【参考方案1】:
    是的,您可以用信号量适当地解决您的问题。它也用于访问共享资源。 并行读取没有问题,但是如果你想实现即使是单次写入,那么你需要小心并妥善处理。 (即使你有并行单写+单读)

【讨论】:

我在原始问题中添加了一个片段。但我不知道如何使读取并发。也许您可以提出一些解决方案? @Sasha 我稍微修改了我的答案,这样会更清楚。无论如何,如果没有并行写入,您不必为读取而烦恼。这是video link about accessing shared resource with semaphore,希望对您有所帮助:)【参考方案2】:

你问:

是否可以借助信号量或锁来解决读写问题?

是的。您提供的方法应该可以做到这一点。

可以使解决方案具有串行写入和串行读取,但是否可以进行并发读取(可以同时进行并发读取)?

那就更复杂了。信号量和锁适用于禁止任何并发访问(包括禁止并发读取)的简单同步。

允许并发读取的方法称为“读写器”模式。但是信号量/锁在不添加各种状态属性的情况下自然不会适用于读写器模式。我们一般使用并发 GCD 队列来完成,并发执行读取,但执行写入时使用屏障(以防止任何并发操作):

class ThreadSafeContainerGCD<Value> 
    private var value: Value
    private let queue = DispatchQueue(label: ..., attributes: .concurrent)

    func get() -> Value 
        queue.sync  value 
    

    func mutate(_ block: @escaping (inout Value) -> Void) 
        queue.async(flags: .barrier)  block(&self.value) 
    

    init(value: Value) 
        self.value = value
    


一些观察:

信号量的效率相对较低。在我的基准测试中,简单的NSLock 更快,而不公平的锁定更是如此。

GCD 读写器模式虽然比信号量模式更有效,但仍然不如简单的锁方法快(即使后者不支持并发读取)。 GCD 开销超过了并发读取和异步写入所带来的好处。

但是对您的用例中的各种模式进行基准测试,看看哪种模式最适合您。见https://***.com/a/58211849/1271826。

【讨论】:

以上是关于如何借助信号量/锁解决数据竞争/读写问题?的主要内容,如果未能解决你的问题,请参考以下文章

多线程之线程同步(互斥锁信号量条件变量和读写锁​)

多线程之线程同步(互斥锁信号量条件变量和读写锁​)

用信号量和读写锁解决读者写者问题

Linux驱动之并发与竞争

线程同步锁死锁递归锁信号量GIL

《Linux内核设计与实现》读书笔记- 内核同步方法