这个多线程列表处理代码是否有足够的同步?

Does this multithreaded list processing code have enough synchronization?

我在 test.cpp 中有此代码:

#include <atomic>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <thread>

static const int N_ITEMS = 11;
static const int N_WORKERS = 4;

int main(void)
{
    int* const items = (int*)std::malloc(N_ITEMS * sizeof(*items));
    for (int i = 0; i < N_ITEMS; ++i) {
        items[i] = i;
    }

    std::thread* const workers = (std::thread*)std::malloc(N_WORKERS * sizeof(*workers));
    std::atomic<int> place(0);
    for (int w = 0; w < N_WORKERS; ++w) {
        new (&workers[w]) std::thread([items, &place]() {
            int i;
            while ((i = place.fetch_add(1, std::memory_order_relaxed)) < N_ITEMS) {
                items[i] *= items[i];
                std::this_thread::sleep_for(std::chrono::seconds(1));
            }
        });
    }
    for (int w = 0; w < N_WORKERS; ++w) {
        workers[w].join();
        workers[w].~thread();
    }
    std::free(workers);

    for (int i = 0; i < N_ITEMS; ++i) {
        std::cout << items[i] << '\n';
    }

    std::free(items);
}

我这样编译linux:

c++ -std=c++11 -Wall -Wextra -pedantic test.cpp -pthread

当 运行 时,程序应该打印:

0
1
4
9
16
25
36
49
64
81
100

我对C++的经验不多,也不太懂原子操作。根据标准,工作线程是否总是正确地平方项值,而主线程是否总是打印正确的最终值?我担心原子变量的更新会与项目值或其他内容不同步。如果是这种情况,我可以更改与 fetch_add 一起使用的内存顺序以修复代码吗?

我觉得很安全。您的 i = place.fetch_add(1) 恰好为每个数组索引分配一次,每个索引分配给一个线程。因此,对于任何给定的数组元素,它仅被单个线程触及,并且对于除结构1.

的位字段之外的所有类型都是guaranteed to be safe

脚注 1:或者标准 unfortunately 要求的 std::vector<bool> 的元素是打包的位向量,打破了 std::vector.[=21= 的一些通常保证]


当工作线程工作时,不需要对这些访问进行任何排序;主线程 join() 在读取数组之前是工作人员,因此工作人员所做的一切“发生在”(在 ISO C++ 标准中)主线程的 std::cout << items[i] 访问之前。

当然,数组元素都是在工作线程启动之前由主线程写入的,但这也是安全的,因为 std::thread constructor 确保父线程中的所有早期内容先于新线程

The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.


在增量上也不需要任何比 mo_relaxed 强的顺序:它是程序中的 only 原子变量,您不需要任何顺序除了整体线程创建和连接之外的任何操作。

它仍然是原子的,因此可以保证 100 个增量将产生数字 0..99,只是不能保证哪个线程得到哪个。 (但是可以保证每个线程都会看到单调递增的值:对于每个原子对象,单独存在一个修改顺序,并且该顺序与程序的一些交错 - 对其修改的顺序一致。)


郑重声明,与让每个工作人员选择连续的指数范围进行平方相比,这效率低得可笑。每个线程只需要 1 个原子访问,或者主线程可以只传递它们的位置。

并且它会避免所有错误的共享效果,即 4 个线程在数组中移动时同时加载和存储到同一个缓存行。

连续范围还可以让编译器使用 SIMD 一次自动矢量化 load/square/store 个多个元素。