C++ shared_mutex 实现

C++ shared_mutex implementation

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

这个想法是,如果没有线程持有锁,则互斥锁保持一个为零的引用计数。如果 > 0,则该值表示具有访问权限的 reader 的数量。如果为 -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);
    }
};

(CAS = Compare And Swap = C++ compare_exchange_weak 函数,在 x86 上通常会编译为 lock cmpxchg instruction,当它拥有独占或缓存行时只能 运行修改后的 MESI 状态)。


lock_shared 看起来不错:只在看起来可能的情况下以只读方式旋转尝试 CAS 比在 CAS 或原子增量上旋转更能提高性能。您已经需要进行只读检查以避免将 -1 更改为 0 并解锁写锁。

在 x86 上,在自旋循环的重试路径中放置一个 _mm_pause() 以避免在退出只读自旋循环时内存顺序错误推测管道核弹,并从其他超线程窃取更少的资源旋转时。 (使用 while() 循环,而不是 do{}while(),所以失败一次后暂停仅 运行s。pause 在 Skylake 上等待大约 100 个循环,所以在快速中避免它-路径。)


我认为 unlock_shared 应该使用 mo_release,而不是 mo_relaxed,因为它需要从共享数据结构中排序负载确保写入器不会在 reader 临界区的加载发生之前开始写入。 (LoadStore reordering is a thing on weakly-ordered architectures, even though x86 only does StoreLoad reordering.) A Release operation will order preceding loads and keep them inside the critical section.


(in write lock): // can memory_order_relaxed be used if only a single thread takes write locks ?

不,您仍然需要将写入保留在临界区内,因此 CAS 仍然需要与来自 unlock_shared.

的(C++ 术语)发布存储同步

https://preshing.com/20120913/acquire-and-release-semantics/ 有一张漂亮的图片,显示了发布存储或获取加载的单向屏障效应。