将线程的结果转储到向量中是线程安全的吗

Is it thread safe to dump results from threads into a vector?

本文关键字:线程 安全 结果 转储 向量      更新时间:2023-10-16

我正在学习C++11的功能,并按照以下几行编写了一些代码

#include <vector>
#include <thread>
using std::thread;
using std::vector;
double Computation(int arg)
{
// some long-running computation
return 42.0;
}
double ConcurrentComputations()
{
const int N = 8; // number of threads
vector<thread> thr;
vector<double> res(N);
const int arg = 123456; // something or other
// Kick off threads which dump their results into res
for(int i=0; i<N; ++i)
thr.push_back(thread ([&res, i, arg]()
{  res[i] =  Computation(arg); } ));
// Wait for them to finish and get results
double sum = 0;
for(int i=0; i<N; ++i) {
thr[i].join();
sum += res[i];
}
return sum;
}

在寒冷的阳光下再次查看它,我认为我真的不应该在lambda函数中引用vector并将数据转储到它中。我认为向量是一个正则数组,并依赖于operator[]的实现来简单地将i添加到&res.front()(也许我应该捕获&res.front(),现在我已经在Stroustrup 4ed中进一步阅读了,我可以看到我可能应该使用futures和promise)。

然而,我的问题是,认为在实践中我可以逃脱我编写的代码的惩罚是否合理?

您的代码实际上很好!(当线程混合在一起时,通常默认情况下会破坏代码,但在这种情况下不会。)

声明vector<double> res(N);将初始化数组,并为所有结果留出足够的空间,因此在循环中永远不会调整向量的大小。

每个线程只写入向量的不同元素,并且在线程构造函数和join()方法中存在隐式内存障碍,它们会按照您的期望保持排序。

现在,至于标准是否真的支持这一点——嗯,可能不支持(我发现的大多数关于std::vector的线程安全引用只保证读取,而不是写入)。捕捉正面也没有帮助,因为无论哪种方式,您仍在对向量的元素进行写入。

(注意,我个人已经成功地跨平台使用了这个确切的模式,没有遇到任何问题。)

我相信您的代码(几乎)是合法的,具有定义良好的行为(请参阅下面对"几乎"部分的解释)。

该标准的重要部分如下:

17.6.5.9/5C++标准库函数不应访问通过其参数或通过其容器参数的元素间接访问的对象,除非调用其规范对这些容器元素所要求的函数。

17.6.5.9/6通过调用标准库容器或字符串成员函数获得的迭代器上的操作可以访问底层容器,但不能修改它。

res[i]等效于*(res.begin() + i)(23.2.3中的表101)这是唯一的问题:我无法从标准文本中证明res.begin()不会修改向量。如果我们假设一个合理的实现不会做这样令人震惊的事情,那么剩下的就顺利了:对begin的调用之后是生成的迭代器上的operator+operator*,这两个迭代器只能访问容器,但不能修改它。对共享对象的并发访问是可以的,它们不会导致数据争用。

res[i]返回一个double&,然后执行对该double对象的分配。这是一个修改,但没有两个线程修改同一个对象,所以这里也没有种族。