【问题标题】:Are concurrent calls to emplace_back() and operator[]() from std::deque thread safe?从 std::deque 线程并发调用 emplace_back() 和 operator[]() 是否安全?
【发布时间】:2017-04-21 10:17:04
【问题描述】:

来自emplace_back()的文档摘录:

  • 迭代器有效性

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

  • 数据竞赛

容器被修改。

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

还有来自operator[]()的文档摘录:

  • 数据竞赛

容器被访问(const 和 non-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 标准(实际上是 November 2014 Working Draft, N4296)关于在 deque 中插入关于引用和迭代器有效性的声明:

  • 23.3.3.4 双端队列修饰符

(...)

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

(...)

【问题讨论】:

  • 没有。 deque 不是线程安全的容器。这里emplace_back() 可能导致重新分配,这几乎关闭了任何类型的线程安全访问容器的书籍。
  • @SamVarshavchik - emplace_back 永远不会导致 deque 中的重新分配。它可能分配一个新页面。即使它确实导致了重新分配,也没有什么说不能使用互斥锁来确保正确的并发访问。 STL 容器关于并发的问题是backpop_back。那些不能被线程安全地拉出并保留它的值。它需要外部锁。 front/pop_front 也一样。
  • emplace_back 更改内容,operator[] 读取它们(读取至少一个指向数据的指针)。这些操作不保证是原子的。因此,它不是线程安全的。

标签: c++ multithreading thread-safety stddeque


【解决方案1】:

同时调用标准类的对象上的任何两个方法都是不安全的,除非两者都是const,或者除非另有说明(例如std::mutex::lock() 的情况)。对此进行了更详细的探讨here

因此,同时使用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 是在启动异步任务之前创建的。

【讨论】:

  • @SergeyA 会有助于说出问题所在而不是“有问题”。
  • 请注意,尽管标准保证了对[] 的多个调用的并发访问,但保证在不同元素上除了关联容器和vector&lt;bool&gt;
【解决方案2】:

编辑说明:这个答案的结论是不正确的 []emplace_back 可以安全地同时使用。 Arne 的回答是正确的。由于 cmets 很有用,所以将其留在这里而不是删除。

Edit2:嗯,从技术上讲,我没有做出这个结论,但它有点暗示。 Arne's 是更好的答案。


尽管我并不完全相信来源,但该文档似乎在说的是,只要您不通过迭代器进行并发访问 other 值,它就是线程安全的。

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

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

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

根据this answer,标准容器除了上述之外没有任何关于线程安全的保证。换句话说,您可以同时访问/修改 不同 元素,但其他任何事情都可能导致数据竞争。然而换句话说,标准容器不是线程安全的,也不提供任何并发保护。

【讨论】:

  • 那么当两个线程同时尝试分配一个新页面时会发生什么?我假设至少需要保护对 emplace_back 的调用,即使其他索引或交互器查找不需要。
  • 请注意,emplace_back 中可能有一条指令以非原子方式更改数据指针。所以,可能会出现emplace_back会改变成员指针,operator[]会访问无效指针(部分指针)的情况,导致UB。
  • @ShmuelH。这是我担心的一件事。在我使用的 STL 实现中,operator[](size_t pos) 是通过简单地返回 * ( begin( ) + pos ) 来实现的。正如文档中所说,emplace_back 将使所有迭代器无效,这让我想到,begin()(可能返回存储的迭代器)的使用可能会搞砸。
  • @Tarc 你对这个非官方的 ref 的信任度超出了你的预期。即使实现只是return ptr[pos],而ptr是一个指向简单数组的指针,也会有问题。出于同样的原因,我之前说过。它可能在某些计算机上工作,但没有任何保证。
  • @Tarc - 比这更糟糕。 begin 可以返回一个新的迭代器,如果在调用begin 和调用返回的迭代器上的+ 之间修改了事情......或* 就此事而言,您仍然可能会遇到问题。 emplace_back[] 不能同时使用而不导致 UB。唯一可以保证的是[] 的并发使用,并且引用永远不会失效。
猜你喜欢
  • 2011-05-05
  • 2021-06-02
  • 2018-07-14
  • 2014-05-23
  • 2015-01-15
  • 1970-01-01
  • 2018-07-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多