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

2022-04-18 00:00:00 multithreading atomic c++ stdatomic

我在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] << '
';
    }

    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::向量的一些常见保证。


在工作线程工作时,不需要对这些访问进行任何排序;主线程join()在读取数组之前对工作线程进行排序,因此工作线程完成的所有操作都发生在主线程std::cout << items[i]访问之前(在ISO C++标准中)。

当然,在启动辅助线程之前,所有数组元素都是由主线程写入的,但这也是安全的,因为std::thread constructor确保父线程中所有较早的内容都发生在新线程中的任何事情之前

构造函数调用的完成与新执行线程上f副本的调用开始同步(如std::memory_order中所定义)。


在增量上也不需要比mo_relaxed更强的任何顺序:它是程序中唯一的原子变量,除了整个线程创建和联接之外,您不需要对任何操作进行任何排序。

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


仅供记录,与让每个工人挑选一个连续的指数范围来平方相比,这效率低得可笑。这将只对每个线程进行1次原子访问,或者主线程可以只将位置传递给它们。

它将避免4个线程在数组中移动时同时加载和存储到同一缓存行的所有虚假共享影响。

连续范围还允许编译器使用SIMD自动向量化,以便一次加载/平方/存储多个元素。

相关文章