C++ shared_mutex实现

LWi*_*sey 11 c++ multithreading mutex c++11 stdatomic

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);
    }
};
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 6

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


lock_shared看起来不错:仅在看起来可能的情况下旋转只读尝试CAS才能比在CAS或原子增量上旋转更好的性能。您已经需要进行只读检查,以避免更改-10并解锁写锁。

在x86上,_mm_pause()在自旋循环的重试路径中放置一个,以避免退出只读自旋循环时出现内存顺序错误的推测管线核,并在自旋时从其他超线程中窃取更少的资源。(请使用while()循环,而不要使用循环,do{}while()因此暂停仅在失败一次后才开始。 pause在Skylake上,稍后等待约100个周期,因此请避免在快速路径中使用它。)


我认为unlock_shared应该使用mo_release,而不是mo_relaxed,因为它需要对共享数据结构中的负载进行排序,以确保编写者在出现来自读者关键部分的负载之前不会开始写入。(即使x86仅对StoreLoad进行重排序,LoadStore的重排序在弱排序的体系结构上也是如此。)Release操作将对先前的负载进行排序 ,并将其保留在关键部分内


(写时lock)://如果只有一个线程需要写锁,可以使用memory_order_relaxed吗?

不,您仍然需要将写操作保留在关键部分内,因此CAS仍需要与(使用C ++术语)与发布存储库同步unlock_shared

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

  • @curiousguy`lock_shared`无法解锁共享互斥锁;如果已处于解锁状态,则只能进行读锁定。您对`unlock_shared`中的排序是正确的,因为它肯定是释放操作。 (2认同)