从 std::d eque 线程对 emplace_back() 和运算符 []() 的并发调用是否安全?

Are concurrent calls to emplace_back() and operator[]() from std::deque thread safe?

本文关键字:并发 调用 安全 是否 运算符 线程 eque emplace std back      更新时间:2023-10-16

emplace_back()文档摘录:

  • 迭代器有效性

与此容器相关的所有迭代器都失效,但指针和引用仍然有效,引用它们在调用之前引用的相同元素。

  • 数据竞赛

容器已修改。

调用不会访问任何包含的元素:并发访问或修改它们是安全的(尽管请参阅上面的迭代器有效性)。

以及operator[]()文档的摘录:

  • 数据竞赛

将访问容器(const 和非 const 版本都不会修改容器)。

元素n可能被访问或修改。同时访问或修改其他元素是安全的。

那么,鉴于deque的某个实例至少有一个元素,通过operator[]()访问它并同时在容器上调用emplace_back()确实是线程安全的吗?

我倾向于说是这样,但无法确定emplace_back()文档中的"访问"是否包括使用operator[](),如下所示:

int access( std::deque< int > & q )
{
return q[ 0 ];
}
void emplace( std::deque< int > & q , int i )
{
q.emplace_back( i );
}

其中两个函数同时调用,或者"访问"仅适用于已经引用或指针的元素:

std::deque< int > q { 1 };
auto * ptr = & q[ 0 ]
std::thread t1 ( [ ptr  ]{ * ref = 0; } );
std::thread t2 ( [ & q ]{ q.emplace_back( 2 ); } );

编辑:为了进一步参考,以下是 C++ 14 标准(实际上是 2014 年 11 月的工作草案,N4296)关于在deque中插入有关引用和迭代器有效性的规定:

  • 23.3.3.4 德克修饰符

(...

  1. 效果:在双端的中间插入会使所有迭代器和对双端元素的引用无效。在双端的任一端插入会使对双端的所有迭代器无效,但对双端元素的引用的有效性没有影响。

(...

同时调用标准类的对象上的任意两个方法是不安全的,除非两者都const,或者除非另有指定(例如,std::mutex::lock()的情况)。这里更详细地探讨了这一点

因此,同时使用emplace_backoperator[]是不安全的。但是,由于您引用的引用/指针有效性规则,您可以安全地使用先前获得的对deque元素的引用,同时调用emplace_back/push_back,例如:

int main()
{
std::deque<int> d;
d.push_back(5);
auto &first = d[0];
auto task = std::async(std::launch::async, [&] { first=3; });
d.push_back(7);
task.wait();
for ( auto i : d )
std::cout << i << 'n';
}

这将安全地输出 3 和 7。请注意,引用first是在启动异步任务之前创建的。

编辑注意:这个答案的结论是不正确的,即[]emplace_back可以同时使用。 阿恩的回答是正确的。 由于评论有用,将其留在这里而不是删除。

编辑2:嗯,从技术上讲,我没有得出这个结论,但它有点暗示。 阿恩的答案更好。


尽管我不完全信任源代码,但本文档似乎在说,只要您不通过迭代器执行此操作,并发访问其他值就是线程安全的。

这种情况的原因是emplace_back不会以任何方式触及其他值。 如果容量太低而无法添加另一个元素,则会分配新页面。 这不会影响其他元素。 因此,通过其他线程使用这些值是安全的。 它永远不会导致数据竞争,因为相同的数据没有被访问/修改。

容器不需要以任何方式是线程安全的,这种情况就是这种情况。 这就像在修改a[0]时访问a[1]。 只要您正确访问/修改该数据(不会导致 UB),它就是安全的操作。 您不需要任何锁来保护,因为您不会同时使用相同的数据。

我更关心的是size. 这很可能是deque中的一个值,由emplace修改并由size读取。 如果没有保护,这将导致数据争用。 文档对此没有任何说明,只涉及元素的访问,这当然可以与调用size同时进行。

根据这个答案,除上述情况外,标准容器对线程安全性没有任何保证。 换句话说,您可以同时访问/修改不同的元素,但其他任何事情都可能导致数据竞争。 但换句话说,标准容器不是线程安全的,并且不提供任何并发保护。