std::forward_list::insert_after 线程安全

Posted

技术标签:

【中文标题】std::forward_list::insert_after 线程安全【英文标题】:std::forward_list::insert_after thread safety 【发布时间】:2017-07-07 01:10:13 【问题描述】:

在共享的std::forward_list 中,如果多个线程保证永远不会使用相同的位置迭代器调用insert_after,它们是否安全?鉴于插入保证不会使其他迭代器无效,并且容器没有 size() 方法,这似乎是安全的,但也许我遗漏了什么?

编辑:

我写了一个小的酷刑测试程序,它似乎在 Clang 中运行良好,没有任何锁定:

#include <forward_list>
#include <iostream>
#include <thread>
#include <vector>

using List = std::forward_list< int >;
using It = List::const_iterator;

void insertAndBranch (List& list, It it, int depth)

    if (depth-- > 0) 
        It newIt = list.insert_after (it, depth);
        std::thread thread0 ([&] insertAndBranch (list, it, depth); );
        std::thread thread1 ([&] insertAndBranch (list, newIt, depth); );
        thread0.join();
        thread1.join();
    


int main()

    List list;
    insertAndBranch (list, list.before_begin(), 8);

    std::vector< It > its;
    for (It it = list.begin(); it != list.end(); ++it) 
        its.push_back (it);
    
    std::vector< std::thread > threads;
    for (It it : its) 
        threads.emplace_back ([&] list.insert_after (it, -1); );
    
    for (std::thread& thread : threads) 
        thread.join();
    

    for (int i : list) 
        std::cout << i << ' ';
    
    std::cout << '\n';

我知道这并不能证明什么,但它让我希望这是安全的。不过,我不确定是否可以在没有标准确认的情况下使用它。

【问题讨论】:

std::list threading push_back, front, pop_front的可能重复 副本是旧的,事情已经改变了。 好吧,OP 没有指定 C++ 标准.. C++14,我刚刚加了一个标签。 任何时候你有多个线程并且其中一个或多个是共享变量的写入者,那么你需要同步。如果没有,你有一个未定义行为的数据竞争。 【参考方案1】:

在共享的std::list 中,多个线程调用插入是否安全 同时,如果他们保证永远不会以相同的方式调用它 位置迭代器?

没有。这不安全……(无论如何)。

插入std::list 需要访问迭代器位置的previous nodenext node,以及列表的size

即使位置相距很远,std::list::size() 的简单事实也需要恒定时间 (C++11)。这意味着每次插入都会更新std::list::size()


编辑:

在共享的std::forward_list 中,多个线程可以安全吗? 如果保证永远不会调用它,则同时调用 insert_after 使用相同的位置迭代器?

这不安全。并且不推荐。没有为 STL 容器设计线程安全。


无论如何,让我们假设一些基本的保证,让我们假设std::forward_list 的一个非常简单的版本:insert_after 修改了迭代器指向的节点,以便该节点现在指向新插入的节点,而新插入的节点指向下一个节点。因此,只有当迭代器彼此相距至少两个节点并且您的分配器是线程安全的时,它才会是“安全的”。

插图:

Initial Forward_list
A -> B -> C -> D -> E

Insert K after C
A -> B -> C x D -> E
           \ /
            K

如您所见,C 已被读取和修改。可以读取D,而插入K


至于为什么我说“至少两个节点离开”,让我们以一个节点离开的情况为例:假设两个线程分别想在C之后插入K,在D之后分别插入M

Initial Forward_list
A -> B -> C -> D -> E

Insert K after C
A -> B -> C x D x E
           \ / \ /
            K   M

来自cppreference:

当一个表达式的求值写入一个内存位置并且 另一个评估读取或修改相同的内存位置, 表达式被称为冲突。

【讨论】:

实际上我在我的代码中使用std::forward_list,而不是std::list。为了简单起见,我使用std::list 写了这个问题,但我现在改变了它。有趣的是std::forward_list 没有 size 方法,所以这会改变你的答案吗? 在双链表中你是正确的,但在单链表中,唯一需要修改的两个节点是位置参数指向的节点和新创建的节点,两者都不会被另一个线程读取或修改。 @atb,我已经更新了我的答案来反驳你的评论(见插图)。您仍然需要至少离开两个节点。不过,这不是保证。使用锁或其他带有线程安全列表的库 在您的第一个图表中,我认为不会读取 D。下一个节点指针将从 C 中获取并提供给 K。我无法想象为什么一个有效的实现需要从 D 中读取。 @atb,我刚刚通过电子邮件与Howard Hinnant 就这个问题进行了交谈。虽然“标准并不能保证它,但他也想不出有什么方法会失败”....libcxx implementation,我认为如果你能履行自己的保证,你就可以走了。 【参考方案2】:

当另一个线程 写入 到列表时,甚至 读取 操作都不是线程安全的(请参阅here 以获得进一步的解释);所以:多次写入不是线程安全的:

虽然从没有锁的列表中读取不会损坏列表,但如果在另一个线程正在读取列表时修改了列表,则任何一个线程都可能损坏(即崩溃或产生不正确的结果)。 em>

您需要某种锁定。期间。

【讨论】:

您可能是对的,但我很想知道在这种特定情况下的原因。 没那么简单。例如,如果我们有一个包含一个元素的前向列表,那么:从一个线程读取其数据 (a = l.begin()-&gt;d;) 并在另一个线程中添加一个元素 (l.insert_after (l.begin(), X);) 不应触发任何竞争条件。这是因为在两个线程中都没有访问共享数据:第一个线程只读取beginbegin-&gt;d,第二个线程写入begin-&gt;next @ShmuelH。 AFAIK 不能保证 begin 是线程安全的,除非您调用 const 版本。 begin() 可以做某事(如果这样做会很疯狂,但我认为标准允许这样做),现在您在调用 begin 时遇到了数据竞赛。 @NathanOliver Luckily that's not the case. @atb 该标准规定,除了少数例外(在我上面的评论中链接),非常量成员函数被认为会修改它被调用的对象的每个部分以进行评估是否存在数据竞争。作为更具体的示例,调试模式实现可能具有额外的簿记结构,insert_after 可以在不同步的情况下写入。

以上是关于std::forward_list::insert_after 线程安全的主要内容,如果未能解决你的问题,请参考以下文章