用原子替换互斥锁C++原子安全<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 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_releasememory_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);

我不确定它能在多大程度上提高性能,如果有的话。

其思想是,只有循环中的最后一个负载才需要是获取操作,这就是围栏所实现的。

相关内容

  • 没有找到相关文章

最新更新