在不同的内存位置同时写入std::deque是否线程安全?

13

我有一个std::deque<std::pair<CustomObj, int>>,在并发块开始时不会改变大小。

并发块会读取deque中的每一个CustomObj并设置它的int值。

我可以保证deque的大小不会改变,因此它不会重新分配内存,并且每个线程只会访问deque的一个内存块而不会访问其他线程的内存块。

同时读写会导致未定义行为吗?我应该把读写放在互斥区域里吗?

3个回答

16

令我惊讶的是,当前标准本身实际上有一个非常清晰的部分涉及到这个问题:

(C++17, 26.2.2 容器数据竞争, 2)

  1. 尽管20.5.5.9存在,但实现必须避免在同一容器中不同元素的包含对象的内容同时被修改时发生数据竞争(除了vector<bool>)。

此外,你可以放心地调用以下访问函数:

  1. 为了避免数据竞争(20.5.5.9),实现应将以下函数视为const:begin、end、rbegin、rend、front、back、data、find、lower_bound、upper_bound、equal_range、at和除关联或无序关联容器之外的operator[]

由于std::deque也不属于例外情况,因此您可以安全地并发调用任何这些函数以获取不同的元素并修改它们。只要确保将对容器本身的任何修改与您并发访问和修改元素的并行区域正确隔离即可。

例如,以下操作是错误的:

std::dequeue<...> dq;
#pragma omp master
{
    ...
    dq.emplace(...);
}
// no implicit barrier here,
// use omp barrier or change to omp single instead of master
#pragma omp for
for (... i; ...)
    dq[i].second = compute(dq[i]);

哦,伙计,你让我很为难,不知道该标记哪个答案。感谢您提供标准报价,这是一个很好的发现。 - quimnuss
我实际上想知道Yakk答案中提到的一般规则如何适用于std::vector<bool>,这让我写下了这个答案,因为它太长了,不适合作为现有评论的回复。 - Zulan
为了保险起见,这个注解也适用于C++11吗? - quimnuss
是的,自从C++11版本以来。 - Zulan
1
你还可以指出deque在插入到任一端时具有引用稳定性,因此如果在代码的同步部分中可以执行auto &ref = dq[i];,那么ref可以与push_back()push_front()等同时访问。不幸的是,迭代器会失效,所以在给定的循环中,你必须事先存储每个单独的指针或引用。 - Arne Vogel

8
只要您能保证deque的大小不变且只有一个线程会写入特定元素,那么是安全的。只有在尝试修改deque(更改其大小)或在多个线程读写deque中的单个元素时才会出现问题。
但您可能会遇到的一件事是称为false sharing的问题。这是一个单个缓存行具有被多个线程使用的元素的过程。由于线程写入将使缓存行变脏,因此整个缓存行需要重新同步,这将损害性能。

4
为了防止虚假共享,尝试使用同一线程访问相邻元素。通常情况下,使用标准的#pragma omp for 即可。 - Zulan
感谢提及,Zultan。这正是我正在做的,所以我感到放心。 - quimnuss
@NathanOliver 关于 std::vector 的这篇答案声称 "在写入时没有并发读取"。 它没有说他是指容器还是容器的元素。 我仍然认为,像你一样,鉴于我的上下文,安全地编写和读取不同的内存位置。 另外,在我的并发块中没有 push_back - quimnuss
1
@quimnuss 我指的是两者都要注意。只要你不让多个线程修改deque本身的状态(如大小),并且只要你不让多个线程读取和修改deque中的单个元素,那么就是安全的。 - NathanOliver

1

所有标准容器的规则是:

  • 在整个容器和每个元素上,可以有多个读取者或一个写入者。
  • 单个元素的读取或修改(不是添加/删除元素)也是对容器的读取操作。

这只是适度地太强了。您可以做一些违反上述规则的小事情,而不会在标准下成为竞争条件。


在标准中,通常用容器上的const方法来表达。读取方法是const,写入方法不是const。例外情况是begin()和end()和data()(只返回迭代器的方法)是非const的,也算作const。
对于迭代和元素访问,它以迭代器失效的术语来表达。许多操作会使迭代器失效,如果迭代器在使用中以无序的方式失效,那么就会出现竞争条件。
以下是一个符合上述经验法则但标准说“可以”的示例:
您可以拥有一个映射和存储在该映射中的值的引用。您可以在另一个线程向映射添加键值对时编辑该值。
由于没有迭代器被映射失效,而且您没有触及键,我相信不存在竞争条件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接