【问题标题】:Why exactly is `Rc` thread unsafe?为什么`Rc`线程是不安全的?
【发布时间】:2020-10-10 11:40:23
【问题描述】:

我正在阅读Rust 编程语言chapter 16 — Shared-State Concurrency。它说:

很遗憾,Rc<T> 不能安全地跨线程共享。当Rc<T> 管理引用计数,它会增加每次调用的计数 克隆并在删除每个克隆时从计数中减去。但它 不使用任何并发原语来确保更改 计数不能被另一个线程中断。

什么意思

以确保更改为 计数不能被另一个线程中断。

我认为计数更改被中断的唯一情况是,如果创建了一个线程,并且它以某种方式出现恐慌/崩溃,因此储物柜永远不会解锁,因此引用计数永远不会减少。我可以想象如果发生恐慌,Rust 会调用范围内每个对象的析构函数。

有人可以帮我澄清一下吗?

【问题讨论】:

    标签: multithreading rust


    【解决方案1】:

    确保对计数的更改不会被另一个线程中断。

    这是一个非常不幸的措辞,而且不准确。真的,干扰是我们最不担心的事情。

    就并发而言,Rust 的内存模型是基于 C11 和 C++11 采用的内存模型。如果想进一步了解内存模型,我只推荐阅读 Preshing 在Weak vs Strong memory models 上的文章;我会尽量在这个答案中做到物质公正。

    你问什么是内存模型?

    粗略地说,内存模型是一种模型,它指定哪些操作可以重新排序,哪些不能。

    可能会发生重新排序:

    • 在优化器中。
    • 在 CPU 中。

    一般来说,出于性能原因,重新排序读/写是。它允许更有效地利用 CPU 并加快进度。但是,某些算法的正确性取决于以特定顺序观察事件的各种线程......因此有时某些读/写不应该重新排序。内存模型和内存排序用于指定编译器和 CPU 正确执行算法应遵守的确切约束。

    CPU怎么会毁了Rc

    忽略增量。

    在弱内存模型中,如果两个不同的内核递增计数器,则其中一个可能会被忽略。

    想象一下给定线程上的以下时间线,其中 CN 表示当前所有者的数量为 N,C0 表示销毁。

     T1 -- Create: C1 --- Clone: C2 -- Drop Clone: C1 --- Drop: C0.
    

    现在,想象一下这个帖子分享了@98​​7654325@:

     T1 -- Create: C1 --- Clone: C2 ---------------C1---- Drop Clone: C0 --- Access **BOOM**.
                      \                                 /
     T2                \_ Clone: C2 -- Drop Clone: C1 _/
                                  ^                 ^
        Only one increment was seen                 But both decrements are
    

    为什么 CPU 会这样做?

    性能。

    强大的内存模型意味着内核之间为了同步高速缓存行而进行大量不必要的交流——这会增加操作的延迟。

    较弱的内存模型允许更少的抖动,从而减少延迟,这意味着程序可以更快地执行或更少的功率。

    如果记忆模型足够强大?

    即使在每次读/写都涉及内存的假设 CPU 上,它仍然可能由于竞争条件而出错。

    具体来说:

    • T1 读取计数 (1),T1 计算递增计数 2,T1 写入计数 (2)。
    • T2 读取计数 (1),T2 计算递增计数 2,T2 写入计数 (2)。

    如果您查看 Rust 中的 AtomicXXX 类型,您会注意到存在许多 RMW(读取-修改-写入)操作,例如 fetch_add原子地读取、递增,然后写。

    原子性很重要,否则可能会出现竞争条件。

    优化器如何破坏Rc

    即使在没有任何寄存器的假设 CPU 上,增量/减量会直接原子地修改内存,但仍然可能出错。

    允许优化器假设在没有内存排序的情况下没有其他执行线程正在观察对内存的写入:毕竟,这样做是未定义的行为。

    因此,优化器完全可以:

    1. 创建Rc 的克隆。
    2. 放弃原件。
    3. 递减计数器 (-2) -- 融合递减以获取乐趣和利润!
    4. 使用克隆。
    5. 增加计数器 (+1)。
    6. 删除克隆。

    如果另一个线程丢弃 (3) 和 (5) 之间的最后一个其他引用,则计数器将达到 0,因此其他线程将丢弃其中的值。

    我不确定我是否理解...

    别担心,您不必这样做!

    Rust 编译器为您提供支持。除非您抽出unsafe,否则它将确保您不会意外引入此类竞争条件。

    至于理解所有这些,那里有很多文献。订购的确切效果是documented,而且为了更大的图景,Preshing 真的很好,我衷心推荐他们的博客。

    【讨论】:

      猜你喜欢
      • 2012-11-20
      • 2013-10-11
      • 1970-01-01
      • 2016-08-14
      • 2023-03-12
      • 1970-01-01
      • 2023-03-30
      • 2012-03-22
      • 2015-07-24
      相关资源
      最近更新 更多