我可以在 C++ 中仅使用 std::atomic 而不使用 std::mutex 安全地共享一个变量吗?

Posted

技术标签:

【中文标题】我可以在 C++ 中仅使用 std::atomic 而不使用 std::mutex 安全地共享一个变量吗?【英文标题】:Can I safelly share a variable across threads in C++ using only std::atomic without std::mutex? 【发布时间】:2018-01-16 15:34:24 【问题描述】:

我编写了一个计算多核素数的程序。 (请忽略这个事实,该算法并非完全有效,数字 0 和 1 在这里被认为是素数。目的只是练习使用线程。)

变量 taken(接下来要测试的数字)正在 8 个线程之间共享。

问题是它可以由一个线程递增,然后由另一个线程立即递增,并在它已经递增两次(或更多次)时由它们读取,因此可以跳过某些值,这是一件坏事。

我以为可以通过使用std::atomic_uint作为变量类型来解决,但我显然错了。

有什么方法可以解决这个问题而无需使用std::mutex,因为我听说它会导致相当大的开销? 源代码:

#include <iostream>
#include <chrono>
#include <vector>
#include <algorithm>
#include <thread>
#include <atomic>

int main()

    const uint MAX = 1000;

    std::vector<bool> isPrime(MAX), done(MAX);
    std::fill(done.begin(), done.end(), false);
    std::atomic_uint taken0; //shared variable
    std::vector<std::thread> threads;
    auto start = std::chrono::system_clock::now();

    for (uint i = 0; i < 8; ++i) 
        threads.emplace_back(
            [&]()
                bool res;
                for (uint tested; (tested = taken.fetch_add(1)) < MAX; )  //taken should be incremented and copied atomically
                    res = true;
                    for (uint k = 2; k < tested; ++k) 
                        if (tested % k == 0) 
                            res = false;
                            break;
                        
                    
                    isPrime[tested] = res;
                    done[tested] = true;
                
            
        );
    
    for (auto & t : threads) 
        t.join();
    

    auto end = std::chrono::system_clock::now();
    auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    uint num = std::count_if(isPrime.begin(), isPrime.end(), [](bool b)return b;);
    uint nDone = std::count_if(done.begin(), done.end(), [](bool b)return !b;);
    std::cout << "number: " << num << " duration: " << milliseconds.count() << '\n';
    std::cout << "not done: " << nDone << '\n';
    for (uint i = 0; i < MAX; ++i)  //Some numbers are always skipped
        if (!done[i]) 
            std::cout << i << ", ";
        
    
    std::cout << '\n';
    return 0;

代码是使用 g++-O3-pthread 参数编译的。 输出:

number: 169 duration: 1
not done: 23
143, 156, 204, 206, 207, 327, 328, 332, 334, 392, 393, 396, 502, 637, 639, 671, 714, 716, 849, 934, 935, 968, 969,

每次输出都不一样。

【问题讨论】:

当心std::vector&lt;bool&gt;,即使从未访问过重叠的索引,它也会给出竞争条件... 循环对我来说似乎是正确的。每次taken 递增时,都会处理其获取的值 (tested)。我猜这个问题是由于std::vector&lt;bool&gt; 造成的数据竞争。试试改成std::vector&lt;char&gt;看看效果。 @jameslarge 我不同意。您可以从多个线程访问不同的内存位置而无需同步。 vector&lt;char&gt;vector&lt;int&gt; 的不同元素是不同的内存位置。您只需要确保不要更新具有相同索引的元素,但这在 OPs 代码中得到保证。 只需将std::vector&lt;bool&gt; isPrime(MAX), isDone(MAX); 替换为std::array&lt;bool, MAX&gt; isPrime, isDone;。您将修复您的数据竞赛,并免费享受美好的时间优化。演示:coliru.stacked-crooked.com/a/36110afafdea0ee8 Write concurrently vector<bool>的可能重复 【参考方案1】:

专业化std::vector&lt;bool&gt; 将值压缩为单个位。因此,单个字节中有多个向量元素,即在单个内存位置中。因此,您的线程会在没有同步的情况下更新相同的内存位置,这是一种数据竞争(因此根据标准是未定义的行为)。

尝试将std::vector&lt;bool&gt; 更改为std::vector&lt;char&gt;

【讨论】:

覆盖std::vector&lt;bool&gt;std::vector&lt;char&gt; 解决了它。非常感谢。

以上是关于我可以在 C++ 中仅使用 std::atomic 而不使用 std::mutex 安全地共享一个变量吗?的主要内容,如果未能解决你的问题,请参考以下文章

C++ std::atomic 在程序员级别有啥保证?

C++ 原子操作 std::atomic<int>

C++并发与多线程 11_std::atomic叙谈std::launch(std::async) 深入

std::atomic 可以安全地与 OpenMP 一起使用吗

在没有 XCHG 的情况下实现自旋锁?

什么是 C++ 原子变量?