同时写入vector<bool>

16

我知道从std::vector中并发读取是可能的,没有"坏"后果,因为这个操作可以被认为是线程安全的。

但是对于写入操作,情况并非总是如此。然而,我在思考是否在我的特定场景中也是如此。

我有一个std::vector<bool>,其中所有元素都初始化为false,给定一个索引数组,我需要将这些元素(vector[index]的每个索引)的值从false更改为true

如果我为每个索引使用不同的线程(有可能某些索引具有相同的值),那么这个操作可以被视为线程安全吗?

如果向量是std::vector<int>(或任何基本类型),并且分配的值始终相同(例如1),这个操作仍然可以被认为是线程安全的吗?


在并发访问时,向量的大小是否保持不变? - decltype_auto
@decltype_auto 是的,向量的大小保持不变。 - Nick
这个问题之前在SO上已经有过讨论。请参见:https://dev59.com/1W855IYBdhLWcg3wbztl https://dev59.com/Qmox5IYBdhLWcg3wr2Po https://dev59.com/YGDVa4cB1Zd3GeqPd303 - Craig Estey
如果你有“索引数组”,则使用带有indirect_array访问的std::valarray<bool>可能适合你。但这与你的并发主题无关。 - decltype_auto
请注意,@CraigEstey,这个问题更具体。 - Nick
@Nick 你说每个索引都有不同的线程,但是有些索引具有相同的值?假设写入的值相同,则可能是线程安全的,但如果是这样,为什么要费心呢?对于某些架构,存在缓存一致性问题。更大的上下文/问题是什么-这可能会有所不同。您是否尝试进行“无锁”操作(而不是互斥量)?在cppcon上有一个长达2小时的视频介绍了这个问题-它并不像看起来那么简单。在某些情况下,加锁版本可能会更快。我已经做过很多多线程,但在这里有点困惑。 - Craig Estey
2个回答

26

vector<bool>的并发写入永远不可行,因为其底层实现依赖于类型为vector<bool>::reference的代理对象,该对象会表现为对bool的引用,但实际上会根据需要获取和更新位域字节。

在没有同步的情况下使用多个线程时,可能会发生以下情况:线程1应该更新一个位,读取包含它的字节。然后线程2读取相同的字节,然后线程1更新一个位并将字节写回,然后线程2更新另一个位并将字节写回,覆盖了线程1的编辑。

这只是可能发生的一种情况,还有其他可能导致相同类型的数据损坏的情况。


vector<int>的情况下,如果您绝对“确定”所有线程都将相同的值写入向量,则此操作通常不会导致数据损坏。 但是,标准当然总是特别小心,并将所有并发访问内存位置(其中至少有一个是写访问)定义为未定义的行为:

如果其中一个修改了一个内存位置而另一个读取或修改了同一个内存位置,则两个表达式计算冲突。— intro.races/2

因此,只要您在两个不同的线程中对同一元素进行任何修改操作,就会出现竞争条件,需要适当的同步,例如使用std::atomic<int>


3
在vector<int>情况下,即使您绝对确定所有线程都将相同的值写入向量中,这个操作也不会导致数据损坏是不正确的。无论要写入的值是否相同,多个线程并发写入同一内存位置都会导致未定义的行为。引用(intro.races/2)的说法:“如果一个表达式修改了内存位置,而另一个表达式读取或修改了相同的内存位置,则这两个表达式之间存在冲突。” - Arne Vogel
澄清一下...这适用于在相同索引处写入相同值的情况。如果所有线程写入不同的索引,正如实际问题的一部分,那么内存位置将是独立的,不存在数据竞争。 - Arne Vogel
@ArneVogel 嗯,标准当然总是要特别小心...你肯定不会期望在任何系统上都以非原子方式写入 int。尽管如此,我仍将这句话放在我的答案中。 - Felix Dombek
如果你依赖于相关标准中不存在的期望,那么如果这些期望在未来被证明是错误的,你的代码将会出现严重问题。经历过足够多次这样的痛苦后,我们应该开始学会避免这种情况。@FelixDombek - David Schwartz
@FelixDombek - 如果我使用一个大小为一字节(而不是一字)的“char”或“uint8_t”类型的向量,它是否是线程安全的? - User2332
@KartikLakhotia 这些是原子性写入的,因此至少任何单个值都不会被并发写入破坏。但标准中的引用仍然适用,因此仍然存在未定义行为。因此,您可能会遇到由编译器优化等引起的问题。 - Felix Dombek

24

[container.requirements.dataraces]/2说:

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

因此,在不使用vector<bool>容器时,您可以安全地从不同线程修改同一标准库容器中的不同元素。


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