【问题标题】:C++ shared_mutex implementationC++ shared_mutex 实现
【发布时间】:2017-05-02 19:06:53
【问题描述】:

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

【问题讨论】:

    标签: c++ multithreading c++11 mutex stdatomic


    【解决方案1】:

    (CAS = Compare And Swap = C++ compare_exchange_weak 函数,在 x86 上通常会编译为 lock cmpxchg instruction,它只能在其拥有处于 Exclusive 或 Modified MESI 状态的缓存行时运行)。


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

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


    我认为unlock_shared 应该使用mo_release,而不是mo_relaxed,因为它需要从共享数据结构中对负载进行排序,以确保写入者之前不会开始写入来自读者关键部分的负载发生。 (LoadStore reordering 是弱排序架构上的东西,即使 x86 只进行 StoreLoad 重新排序。)A Release operation will order preceding loads and keep them inside the critical section


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

    不,您仍然需要将写入保持在关键部分内,因此 CAS 仍然需要与来自unlock_shared 的(用 C++ 术语)发布存储同步。

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

    【讨论】:

    • 我不确定 unlock_shared 中的内存顺序,但我的理由是它并没有真正“释放”任何东西,因为它具有只读访问权限并且无法更改它保护的数据
    • @LWimsey:是的,考虑加载排序比考虑存储排序更难,但这是真实的。负载在从 L1 缓存读取数据的那一刻变得全局可见。 (因为那是它从全局一致的缓存复制到单个 CPU 的无序内核的时候。)
    • @LWimsey 你是说 lock_shared 可以立即进行解锁操作吗?疯了,但你似乎暗示订购 WRT 到 unlock_shared 并不重要......
    • @curiousguy lock_shared 无法解锁共享互斥锁;如果已经处于解锁状态,它只能使用读锁。您对unlock_shared 中的排序是正确的,这肯定是发布操作。
    • 当值为”是什么?
    猜你喜欢
    • 2012-12-27
    • 1970-01-01
    • 2011-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-07
    • 1970-01-01
    • 2014-02-02
    相关资源
    最近更新 更多