在单个链表线程中擦除和插入是不是安全?

Posted

技术标签:

【中文标题】在单个链表线程中擦除和插入是不是安全?【英文标题】:Is erasing and inserting in a single linked list thread safe?在单个链表线程中擦除和插入是否安全? 【发布时间】:2014-01-20 03:56:49 【问题描述】:

使用 std::forward_list 在擦除 插入时是否存在任何数据竞争?例如,我有一个线程只在列表末尾添加新元素,而另一个线程遍历(相同)列表并可以从中删除元素。

根据我对链表的了解,每个元素都有一个指向下一个元素的指针,所以如果我删除最后一个元素,同时我插入一个新元素,这会导致数据竞争还是做这些容器的工作方式不同(或者它们是否处理这种可能性)?

如果是数据竞争,是否有(简单而快速)的方法来避免这种情况? (注意:插入的线程是两者中速度最关键的。)

【问题讨论】:

如果你不得不问,一个好的经验法则是:不,它不是线程安全的。实际上,从头开始,这始终是一个很好的经验法则。 该标准是否对线程安全做出任何保证?如果不是,那么依赖它是不好的,即使它现在可以在你的编译器上运行。 @MarkRansom:是的,该标准确实为容器提供了线程安全保证。但是,它们相当薄弱,不足以涵盖问题中的用户案例(另请参阅我的回答)。 如果你有C++11 可用(对于std::atomic,编写一个对单个生产者/单个消费者安全的容器(请参阅:github.com/chadkler/hipoconcon/blob/master/inc/ringbuffer.h)以供免费使用相对容易(但未经过全面审查/测试)示例。它不是 list,而是 bounded ring buffer。确保 initial_size 是 2 的幂。 拜托,这样啊,你不知道你应该使用 Boost 吗??? 【参考方案1】:

标准 C++ 库容器有线程安全保证,但它们往往不是人们会考虑的那种线程安全保证(但是,人们期望错误的东西会导致错误)。标准库容器的线程安全保证大致是(相关部分 17.6.5.9 [res.on.data.races]):

    您可以拥有任意数量的容器读取器。真正符合reader的条件有点微妙但大致相当于const成员函数的用户加上一些非const成员只读取数据(读取数据的线程安全不是任何容器问题,即 23.2.2 [container.requirements.dataraces] 指定可以在容器不引入数据竞争的情况下更改元素。 如果一个容器有一个写入器,则另一个线程中不应有其他容器的读取器或写入器。

也就是说,读取容器的一端并写入另一端是不是线程安全的!事实上,即使实际的容器更改不会立即影响阅读器,当将一条数据从一个线程传送到另一个线程时,您总是需要某种形式的同步。也就是说,即使你可以保证消费者不erase()生产者当前insert()s的节点,也会存在数据竞争。

【讨论】:

文档说在插入和擦除时访问任何元素是安全的(假设它不是您正在擦除的元素),因为它不会以任何方式修改它们(事实上,所有迭代器所有其他元素仍然有效)。它只是我找不到可靠信息的具体用例。 @latreides:如果你的实现做出了超出我上面所说的保证,那么这样做是免费的。但是,对任何标准库容器的并发访问和修改(如更改对象的数量或位置)可能会引入数据竞争。 23.2.2 [container.requirements.dataraces] 可能是您所指的:您可以更改存储在容器中的 elements 而不会容器引入数据竞争(注意,您仍然需要确保元素访问不受种族限制)。【参考方案2】:

不,forward_list 和任何其他 STL 容器都不是线程安全的写入。您必须提供同步,以便在发生写入时没有其他线程读取或写入容器。只有同时读取是安全的。

最简单的方法是在插入发生时使用互斥锁来锁定对容器的访问。以可移植方式执行此操作需要 C++ 11 (std::mutex) 或特定于平台的功能(mutexes in Windows,在 Linux/Unix 中可能是 pthreads)。

【讨论】:

Windows 中的互斥锁(不使用 futexes)对于这个用例来说太慢了。文档说 std::list 和 std::forward_list 对于几乎所有操作都是线程安全的(例如,您可以从列表中删除,并访问列表的任何其他元素,而无需数据竞争)但我找不到有关此特定案例的信息。 @latreides 你必须假设它不安全。 @latreides 不看标准,如果我不得不猜测,我会说由于forward_list 的迭代器失效规则几乎每个案例都是线程-安全,除了你关心的那个。所以你必须求助于提供同步。如果您不关心可移植性,您可以使用CRITICAL_SECTION(假设这不是多进程场景)。除非有实际的争用,否则这些都是相当轻量级的。 我发布了这个问题,所以我可以停止假设。我正在努力确定,以便我可以做出相应的计划。 @latreides 然后停止假设......它是安全的,因为文档没有明确说明它是安全的。这就是它的工作原理。除非您想查看容器的实现,否则您是在对实现做出假设,而不是依赖于接口契约。【参考方案3】:

除非您使用的 STL 版本明确声明它是线程安全的,否则容器不是线程安全的。

默认情况下使通用容器线程安全是很少见的,因为它会给不需要线程安全访问容器的用户带来性能损失,而这是迄今为止的正常使用模式。

如果线程安全对您来说是个问题,那么您需要用锁包围您的代码,或者使用专为多线程访问而设计的数据结构。

【讨论】:

是否有专门为多线程访问设计的类似 std::forward_list 的容器(这是标准的一部分)?【参考方案4】:

std 容器并不是线程安全的。

您应该小心保护它们以进行修改操作。

【讨论】:

不完全正确,标准库容器对于并发读取访问是线程安全的。只有当你修改容器时,你必须提供外部同步。 但是如果你保护修改操作,那么读访问必须被保护。 @StephaneRolland 具有 std::forward_list 读取访问权限是安全的,即使容器被修改,只要您使用迭代器而不是索引。特别是对于这个容器,除了特别修改/擦除/等的迭代器之外的所有迭代器即使在容器修改之后仍然有效。

以上是关于在单个链表线程中擦除和插入是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

C++并发编程:线程安全链表

C++并发编程:线程安全链表

HashMap多线程不安全问题总结

在本机反应中擦除 AsyncStorage

在fabric js中擦除iText时显示光标线代替擦除的字符

从向量中擦除指针