C++ shared_mutex 实现
C++ shared_mutex implementation
boost::shared_mutex
或 std::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 上通常会编译为 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/ 有一张漂亮的图片,显示了发布存储或获取加载的单向屏障效应。
boost::shared_mutex
或 std::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 上通常会编译为 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
.
https://preshing.com/20120913/acquire-and-release-semantics/ 有一张漂亮的图片,显示了发布存储或获取加载的单向屏障效应。