#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
using namespace std;
const int FLAG1 = 1, FLAG2 = 2, FLAG3 = 3;
int res = 0;
atomic<int> flagger;
void func1()
{
for (int i=1; i<=1000000; i++) {
while (flagger.load(std::memory_order_relaxed) != FLAG1) {}
res++; // maybe a bunch of other code here that don't modify flagger
// code here must not be moved outside the load/store (like mutex lock/unlock)
flagger.store(FLAG2, std::memory_order_relaxed);
}
cout << "Func1 finishedn";
}
void func2()
{
for (int i=1; i<=1000000; i++) {
while (flagger.load(std::memory_order_relaxed) != FLAG2) {}
res++; // same
flagger.store(FLAG3, std::memory_order_relaxed);
}
cout << "Func2 finishedn";
}
void func3() {
for (int i=1; i<=1000000; i++) {
while (flagger.load(std::memory_order_relaxed) != FLAG3) {}
res++; // same
flagger.store(FLAG1, std::memory_order_relaxed);
}
cout << "Func3 finishedn";
}
int main()
{
flagger = FLAG1;
std::thread first(func1);
std::thread second(func2);
std::thread third(func3);
first.join();
second.join();
third.join();
cout << "res = " << res << "n";
return 0;
}
我的程序中有一个片段与此示例类似。基本上,有3个线程:输入程序、处理器和输出程序。我发现使用atomic的繁忙等待比使用条件变量(例如:(让线程进入睡眠更快
std::mutex commonMtx;
std::condition_variable waiter1, waiter2, waiter3;
// then in func1()
unique_lock<std::mutex> uniquer1(commonMtx);
while (flagger != FLAG1) waiter1.wait(uniquer1);
但是,示例中的代码安全吗?当我运行时,它会给出正确的结果(-std=c++17 -O3
标志(。然而,我不知道编译器是否可以将我的指令重新排序到原子检查/设置块之外,尤其是使用std::memory_order_relaxed
。如果它是不安全的,那么有什么方法可以使它安全,同时比互斥锁更快?
编辑:保证线程数<CPU核数
std::memory_order_relaxed
不会保证内存操作的顺序,除了原子本身。
因此,您的所有res++;
操作都是数据竞赛,并且您的程序具有未定义的行为。
示例:
#include<atomic>
int x;
std::atomic<int> a{0};
void f() {
x = 1;
a.store(1, std::memory_order_relaxed);
x = 2;
}
带有-O2
的x86_64上的Clang 13将此函数编译为
mov dword ptr [rip + a], 1
mov dword ptr [rip + x], 2
ret
(https://godbolt.org/z/hxjYeG5dv)
即使在高速缓存一致性平台上,在第一和第二mov
之间,另一个线程也可以观察到a
被设置为1
,但x
没有被设置为1
。
必须使用memory_order_release
和memory_order_acquire
(或顺序一致性(来替换互斥。
(我应该补充一点,我还没有详细检查您的代码,所以我不能说在您的特定情况下,简单地替换内存顺序就足够了。(
如另一个答案中所述,不同线程中的res++;
彼此不同步,并导致数据争用和未定义的行为。这可以用线程消毒液进行检查。
要解决此问题,需要使用memory_order_acquire
进行加载,使用memory_order_release
进行存储。修复也可以用线程消毒液来确认。
while (flagger.load(std::memory_order_acquire) != FLAG1) {}
res++;
flagger.store(FLAG2, std::memory_order_release);
或者,循环后flagger.load(std::memory_order_acquire)
可以替换为flagger.load(std::memory_order_relaxed)
,然后是std::atomic_thread_fence(std::memory_order_acquire);
。
while (flagger.load(std::memory_order_relaxed) != FLAG1) {}
std::atomic_thread_fence(std::memory_order_acquire);
res++;
flagger.store(FLAG2, std::memory_order_release);
我不确定它能在多大程度上提高性能,如果有的话。
其思想是,只有循环中的最后一个负载才需要是获取操作,这就是围栏所实现的。