获取释放内存顺序与顺序一致性不同的实际例子是什么?
What's are practical example where acquire release memory order differs from sequential consistency?
显然,顺序一致的原子操作在有效的可观察行为方面不同于有效的 C++ 程序中的仅获取-释放操作。定义在 C++ 标准(自 C++11 起)或 here 中给出。
但是,我从未遇到过获取-释放语义不足且需要顺序一致性的算法或数据结构的真实示例。
什么是现实世界算法或数据结构的实用示例,其中需要顺序一致性并且获取-释放内存顺序不够?
请注意,即使 。
Peterson 的算法是需要顺序一致性的示例。
在互斥之前的日子里,该算法用于为单个线程提供对受保护区域的访问权限。
该算法仅适用于 2 个线程,每个线程管理一个表示访问受保护区域的意图的标志。
如果两者都在(大约)同时设置标志,则两者都会后退并重试。
真正的算法更高级,因为它使用 'turn' 标志来管理公平访问,但是为了显示 seq/cst 和 acq/rel 之间的区别,这是没有必要的。
下面是 Peterson 算法的 ready-to-compile 简化版本,它实际上表明如果使用比顺序一致性弱的算法,该算法就会被破坏。
有趣的是,这在 X86 上是不平衡的,因为该平台允许 store-loads 被重新排序。
store-load 重新排序的问题在于,两个线程都可以通过将它们的 me
标志设置为 true
来表达它们访问受保护区域的意图,同时它们都从 him
标志(因为该值尚未传播到两个线程)并进入保护区。这对于顺序一致性是不可能的。
使用 gcc
,我必须使用 -O3
优化进行编译才能触发 assert
,而 clang
则没有必要。
两个编译器都使用不同的方法来实现顺序一致性。
#include <thread>
#include <atomic>
#include <assert.h>
std::atomic<bool> flag1{false};
std::atomic<bool> flag2{false};
std::atomic<int> counter{0};
// Change these to memory_order_seq_cst to fix the algorithm
static const auto store_ordering = std::memory_order_release;
static const auto load_ordering = std::memory_order_acquire;
void busy(int n)
{
auto &me = (n==1) ? flag1 : flag2;
auto &him = (n==1) ? flag2 : flag1;
for (;;)
{
for (;;)
{
me.store(true, store_ordering);
if (him.load(load_ordering) == false)
{
// got the 'lock'
break;
}
// retention, no wait period -> busy loop
me.store(false, store_ordering);
}
int tmp = counter.fetch_add(1, std::memory_order_relaxed);
assert(tmp == 0);
/*
* critical area
*/
tmp = counter.fetch_sub(1, std::memory_order_relaxed);
assert(tmp == 1);
me.store(false, store_ordering);
}
}
int main()
{
std::thread t1{busy, 1};
std::thread t2{busy, 2};
t1.join();
t2.join();
}
显然,顺序一致的原子操作在有效的可观察行为方面不同于有效的 C++ 程序中的仅获取-释放操作。定义在 C++ 标准(自 C++11 起)或 here 中给出。
但是,我从未遇到过获取-释放语义不足且需要顺序一致性的算法或数据结构的真实示例。
什么是现实世界算法或数据结构的实用示例,其中需要顺序一致性并且获取-释放内存顺序不够?
请注意,即使
Peterson 的算法是需要顺序一致性的示例。
在互斥之前的日子里,该算法用于为单个线程提供对受保护区域的访问权限。 该算法仅适用于 2 个线程,每个线程管理一个表示访问受保护区域的意图的标志。 如果两者都在(大约)同时设置标志,则两者都会后退并重试。 真正的算法更高级,因为它使用 'turn' 标志来管理公平访问,但是为了显示 seq/cst 和 acq/rel 之间的区别,这是没有必要的。
下面是 Peterson 算法的 ready-to-compile 简化版本,它实际上表明如果使用比顺序一致性弱的算法,该算法就会被破坏。
有趣的是,这在 X86 上是不平衡的,因为该平台允许 store-loads 被重新排序。
store-load 重新排序的问题在于,两个线程都可以通过将它们的 me
标志设置为 true
来表达它们访问受保护区域的意图,同时它们都从 him
标志(因为该值尚未传播到两个线程)并进入保护区。这对于顺序一致性是不可能的。
使用 gcc
,我必须使用 -O3
优化进行编译才能触发 assert
,而 clang
则没有必要。
两个编译器都使用不同的方法来实现顺序一致性。
#include <thread>
#include <atomic>
#include <assert.h>
std::atomic<bool> flag1{false};
std::atomic<bool> flag2{false};
std::atomic<int> counter{0};
// Change these to memory_order_seq_cst to fix the algorithm
static const auto store_ordering = std::memory_order_release;
static const auto load_ordering = std::memory_order_acquire;
void busy(int n)
{
auto &me = (n==1) ? flag1 : flag2;
auto &him = (n==1) ? flag2 : flag1;
for (;;)
{
for (;;)
{
me.store(true, store_ordering);
if (him.load(load_ordering) == false)
{
// got the 'lock'
break;
}
// retention, no wait period -> busy loop
me.store(false, store_ordering);
}
int tmp = counter.fetch_add(1, std::memory_order_relaxed);
assert(tmp == 0);
/*
* critical area
*/
tmp = counter.fetch_sub(1, std::memory_order_relaxed);
assert(tmp == 1);
me.store(false, store_ordering);
}
}
int main()
{
std::thread t1{busy, 1};
std::thread t2{busy, 2};
t1.join();
t2.join();
}