下面的代码显示了通过原子标志获取共享状态的两种方法。读者线程调用poll1()
或poll2()
来检查作者是否已发信号通知。
民意调查选项#1:
bool poll1() {
return (flag.load(std::memory_order_acquire) == 1);
}
民意调查选项#2:
bool poll2() {
int snapshot = flag.load(std::memory_order_relaxed);
if (snapshot == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
return true;
}
return false;
}
请注意,选项#1为presented in an earlier question,选项#2类似于example code at cppreference.com。
假设读者同意仅在poll
函数返回true
时检查共享状态,那么两个poll
函数是否正确且等效?
选项#2是否有标准名称?
每个选项有哪些优点和缺点?
选项#2在实践中可能更有效吗?是否有可能降低效率?
以下是一个完整的工作示例:
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
int x; // regular variable, could be a complex data structure
std::atomic<int> flag { 0 };
void writer_thread() {
x = 42;
// release value x to reader thread
flag.store(1, std::memory_order_release);
}
bool poll1() {
return (flag.load(std::memory_order_acquire) == 1);
}
bool poll2() {
int snapshot = flag.load(std::memory_order_relaxed);
if (snapshot == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
return true;
}
return false;
}
int main() {
x = 0;
std::thread t(writer_thread);
// "reader thread" ...
// sleep-wait is just for the test.
// production code calls poll() at specific points
while (!poll2()) // poll1() or poll2() here
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << x << std::endl;
t.join();
}
答案 0 :(得分:2)
我想我可以回答你的大多数问题。
两种选择当然都是正确的,但它们并不完全相同,因为独立栅栏的适用性稍大(它们在您想要完成的任务方面相当,但是独立栅栏在技术上可以适用于其他事情 - 想象一下这段代码是否内联)。 this post by Jeff Preshing中解释了独立围栏与商店/取景围栏的不同之处。
据我所知,选项#2中的check-then-fence模式没有名称。但这并不罕见。
在性能方面,使用x64(Linux)上的g ++ 4.8.1,两个选项生成的程序集归结为单个加载指令。这并不奇怪,因为x86(-64)加载和存储都在硬件级别具有获取和释放语义(x86以其非常强大的内存模型而闻名)。
但是,对于ARM,内存屏障会编译为实际的单个指令,会生成以下输出(使用带有-O3 -DNDEBUG
的{{3}}):
while (!poll1());
:
.L25:
ldr r0, [r2]
movw r3, #:lower16:.LANCHOR0
dmb sy
movt r3, #:upper16:.LANCHOR0
cmp r0, #1
bne .L25
while (!poll2());
:
.L29:
ldr r0, [r2]
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
cmp r0, #1
bne .L29
dmb sy
您可以看到唯一的区别是放置同步指令(dmb
)的位置 - 在poll1
的循环内部,以及在poll2
之后。所以poll2
在这个真实世界的情况下确实更有效率:-)(但是请进一步阅读为什么这可能无关紧要,如果它们在循环中被调用以阻止直到标志发生变化。)
对于ARM64,输出是不同的,因为存在内置屏障的特殊加载/存储指令(ldar
- &gt; load-acquire)。
while (!poll1());
:
.L16:
ldar w0, [x1]
cmp w0, 1
bne .L16
while (!poll2());
:
.L24:
ldr w0, [x1]
cmp w0, 1
bne .L24
dmb ishld
同样,poll2
导致一个没有障碍的循环,一个在外面,而poll1
每次都会形成障碍。
现在,哪一个实际上更高性能需要运行基准测试,不幸的是我没有为此设置。反直觉地,poll1
和poll2
在这种情况下可能最终同样有效,因为如果标志变量是一个,花费额外的时间等待记忆效应在循环内传播可能实际上不会浪费时间那些需要传播的效果(即,即使对poll1
的个别(内联)调用比poll2
调用的时间长,循环退出所需的总时间也可能相同)。当然,这是假设一个循环等待标志改变 - 单独调用poll1
做需要比单独调用poll2
更多的工作。
所以,我认为总的来说,只要编译器可以消除分支,poll2
永远不会比poll1
效率低得多,并且通常可以更快,这是相当安全的。当它被内联时(至少这三种流行的架构似乎就是这种情况)。
我的(略有不同)测试代码供参考:
#include <atomic>
#include <thread>
#include <cstdio>
int sharedState;
std::atomic<int> flag(0);
bool poll1() {
return (flag.load(std::memory_order_acquire) == 1);
}
bool poll2() {
int snapshot = flag.load(std::memory_order_relaxed);
if (snapshot == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
return true;
}
return false;
}
void __attribute__((noinline)) threadFunc()
{
while (!poll2());
std::printf("%d\n", sharedState);
}
int main(int argc, char** argv)
{
std::thread t(threadFunc);
sharedState = argc;
flag.store(1, std::memory_order_release);
t.join();
return 0;
}