[英]C++ atomic is it safe to replace a mutex with an atomic<int>?
#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 finished\n";
}
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 finished\n";
}
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 finished\n";
}
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 个线程:输入器、处理器和输出器。 我发现使用原子的忙等待比使用条件变量使线程进入睡眠状态要快,例如:
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
不能保证 memory 操作的顺序,除了原子本身。
你所有的res++;
因此,操作是数据竞争,您的程序具有未定义的行为。
例子:
#include<atomic>
int x;
std::atomic<int> a{0};
void f() {
x = 1;
a.store(1, std::memory_order_relaxed);
x = 2;
}
Clang 13 on x86_64 with -O2
将此 function 编译为
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
(或顺序一致性)来替换互斥锁。
(我应该补充一点,我没有详细检查你的代码,所以我不能说在你的具体情况下简单地替换 memory 订单就足够了。)
如另一个答案中所述, 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);
我不确定它在多大程度上提高了性能,如果有的话。
这个想法是,只有循环中的最后一个加载需要是一个获取操作,这就是栅栏实现的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.