这个多线程列表处理代码是否有足够的同步?
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 个多个元素。
我在 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.
脚注 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 个多个元素。