C++ 共享互斥锁实现

14

boost::shared_mutexstd::shared_mutex(C ++ 17)可用于单写多读访问。作为教育练习,我编写了一个简单的实现,使用自旋锁并具有其他限制(例如公平策略),但显然不适用于实际应用。

这个想法是互斥量保持引用计数,如果没有线程持有锁,则引用计数为零。如果> 0,则该值表示可以访问的读者数量。如果为-1,则单个写入者可以访问。

这是一个正确的实现(特别是使用了最小的内存顺序),没有数据竞争吗?

#include <atomic>

class my_shared_mutex {
    std::atomic<int> refcount{0};
public:

    void lock() // write lock
    {
        int val;
        do {
            val = 0; // Can only take a write lock when refcount == 0

        } while (!refcount.compare_exchange_weak(val, -1, std::memory_order_acquire));
        // can memory_order_relaxed be used if only a single thread takes write locks ?
    }

    void unlock() // write unlock
    {
        refcount.store(0, std::memory_order_release);
    }

    void lock_shared() // read lock
    {
        int val;
        do {
            do {
                val = refcount.load(std::memory_order_relaxed);

            } while (val == -1); // spinning until the write lock is released

        } while (!refcount.compare_exchange_weak(val, val+1, std::memory_order_acquire));
    }

    void unlock_shared() // read unlock
    {
        // This must be a release operation (see answer)
        refcount.fetch_sub(1, std::memory_order_relaxed);
    }
};
1个回答

7

(CAS = 比较并交换 = C++ compare_exchange_weak 函数,对于 x86 平台通常会编译成 lock cmpxchg 指令,该指令只能在独占或修改的 MESI 状态下拥有缓存行时运行)。


lock_shared看起来很不错:在只读时旋转尝试CAS,只有在可能性很大时才更好地提高了性能,而不是在CAS或原子增量上旋转。 您已经需要进行只读检查以避免将-1更改为0并解锁写锁。

在x86上,在旋转循环的重试路径中放置_mm_pause(),以避免在退出只读旋转循环时发生内存顺序错误的管道故障,并减少对其他超线程的资源占用。 (使用while()循环,而不是do{}while(),因此暂停仅在失败后运行一次。在Skylake及更高版本上等待约100个周期的pause,因此避免在快速路径中使用它。)


我认为unlock_shared应该使用mo_release而不是mo_relaxed,因为它需要对来自共享数据结构的加载进行排序,以确保在读者临界区发生加载之前,写者不会开始写入。 (即使x86只进行StoreLoad重新排序,LoadStore重排序在弱序架构上也是一件事情。) 释放操作将对前面的加载进行排序,并将其保留在临界区内


(在写入lock中): // 如果只有一个线程获取写锁,是否可以使用memory_order_relaxed?

不行,您仍然需要将写操作放在临界区内,因此CAS仍需要与unlock_shared的release-stores同步(按C++术语)。

https://preshing.com/20120913/acquire-and-release-semantics/有一张漂亮的图片,展示了release-store或acquire-load的单向屏障效果。


我不确定unlock_shared中的内存顺序,但我的推理是它并没有真正“释放”任何东西,因为它只有只读访问权限,无法更改所保护的数据。 - LWimsey
@LWimsey:是的,考虑加载顺序比存储顺序更困难,但这是真实存在的。当一个加载操作从L1缓存中读取数据时,它变得在全局可见。(因为这时它会从全局一致性缓存中复制到单个CPU的乱序核心中。) - Peter Cordes
@LWimsey 你是在说lock_shared可以立即执行解锁操作吗?这太疯狂了,但你似乎暗示解锁顺序并不重要... - curiousguy
2
@curiousguy lock_shared不能解锁共享的互斥锁;如果已经在未加锁状态下,它只能获取读锁。你对于unlock_shared中的顺序是正确的,它必须是一个释放操作。 - LWimsey
“当值为”是什么? - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接