【问题标题】:C - safety when accessing an integer variable: 1 writer, N readersC - 访问整数变量时的安全性:1 个写入器,N 个读取器
【发布时间】:2015-04-26 23:13:39
【问题描述】:

我有一个仅由一个线程更新的“静态 64 位整数变量”。 所有其他线程只从它读取。

出于安全原因,我应该使用原子操作(例如“__sync_add_and_fetch”)保护这个变量吗?

或者直接从(resp. to)读取(resp.write)它可以吗?

我仍然很困惑,因为我没有找到明确的答案。我不知道我是否必须保护它:

  1. 仅在写入时
  2. 用于写入和读取 (__sync_add_and_fetch(V, 0))
  3. 根本不需要保护它

谢谢。

【问题讨论】:

    标签: c multithreading thread-safety atomic


    【解决方案1】:

    通常,读取和写入存储位置的原子性是相同的。

    也就是说,如果一个位置不能原子地写入,它也不能原子地读取,反之亦然。

    如果需要特殊的原子写入,除非读取也是原子的,否则使用它是没有意义的。

    例如,假设使用普通读取来读取 64 位位置,这需要两次 32 位访问。假设写入发生在这两个访问之间。读取将从新值中获取第二个 32 位,并将其与过时的前 32 位组合。写入不能在读取的两半之间进行的唯一方法是读取是原子的。原子读取知道如何正确地与原子写入交互以防止这种情况发生。

    您可能会对此规则的“例外”感到困惑。在某些系统中,您可能会看到原子更新(例如增量)与普通读取混合。这是基于读取和写入实际上是原子的假设。特殊的原子增量仅用于使读/修改/写周期从并发写入者的角度看起来不可分割:如果 N 个写入者几乎同时执行此递增,则确保该位置增加 N。

    有时您可能还会看到使用普通读取的正确优化,即使底层数据类型不是原子访问的。在这种情况下,算法并不关心它读取的是“半生不熟”的值。

    例如,为了简单地监视内存位置以检测更改,您不需要原子读取。检测更改不需要检索正确的值。例如 0x00000000 更新为 0x00010001,但非原子读取观察到中间值 0x00010000,这仍然足以检测到位置已更改。

    如果您必须确保读者永远不会看到半生不熟的值,那么请使用原子读写。

    还有其他问题,例如订购。假设写者更新了两个位置,A 和 B。在某些计算系统中,读者可能在 A 之前观察到 B 的更新。除了任何原子指令之外,还必须使用特殊的“屏障”或“栅栏”指令更新说明。

    在更高级的语言中,这些障碍的 API 可能内置在某些原子操作的语义中,因此您最终可能只是为了这些障碍而使用原子指令,即使数据是原子的.

    【讨论】:

    • 多线程的一个方面是 C 的标准存储限定符不知道线程的问题。因此,编译器可以轻松优化对共享变量的写入。它还可以从内存中删除读取。这可以通过 volatile 来规避,无论如何这都是必需的。对于以 0x00010001 给出的示例,这取决于。检测到变化就足够了,但您不要仅仅为此使用整数变量,而是要获得一个有价值的值。除非您使用格雷码计数器,否则您确实需要为此进行原子读写。如果没有,则留给编译器;它应该最清楚。
    • @Olaf 写道:“检测变化就足够了,但你不能仅仅为此使用整数变量,而是要获得一个有价值的值”。是的,所以在一种情况下可以使用常规读取,而在另一种情况下可以使用原子读取。
    • @Olaf 写道“因此,编译器可以轻松优化对共享变量的写入。它还可以从内存中删除读取”。确实;你必须使用支持并发编程的编译器:它支持volatile,生成可重入函数链接等等。
    • @Kaz:声明我的变量怎么样:static volatile uint64_t z = 0;我正在使用 gcc 版本 4.9.2。
    • @Kaz:找到一篇关于Volatile: Almost Useless for Multi-Threaded Programming 的英特尔文章 不确定这个关键字现在是否仍然适用(除非有特殊情况)。
    【解决方案2】:

    作为一个简单的答案,我建议您仅在写入时保护它(不直接写入不可以)。

    虽然您知道,如果您正在做类似的事情并且线程未同步,您可能每次都会得到不同的结果。

    PS。我希望这是一个评论,但我的代表太低了

    【讨论】:

      【解决方案3】:

      是的。你需要一个读写锁。这正是他们所做的。写作时的块和其他读者可以随意阅读的内容

      如果你使用 boost,我相信它是 boost::shared_mutex

      C++ 目前不支持读写锁,但您可以自己实现它们。

      有关读写器锁的更多信息,请查看here

      【讨论】:

      • 这就像拿大锤敲碎坚果一样。而且,我们也在谈论 C,而不是 C++(至少你的答案应该适用于两者。
      【解决方案4】:

      如果目标架构支持 64 位值的原子读/写,那么您根本不需要保护:编译器会自动使用相应的指令。在这种情况下最好使用 'volatile' 修饰符,这样可以防止编译器在单个读取器中读取两次值。

      否则,您应该在更新线程中的写入和读取线程中的读取周围使用某种关键部分。可能是互斥锁、读写锁、seq-lock等。

      似乎纯“C”没有精确的方法来检测给定机器是否支持64位值的原子读/写。但是有很多方法可以检测机器本身是否是 64 位的。您可以放心地假设 64 位机器支持 64 位原子读/写。

      【讨论】:

      • volatile 不是可选的!并且它不会阻止读取两次,而是指示编译器可以在正常(顺序)程序流程之外修改变量。因此,如果读取两次,它将最终被读取两次(并按编程顺序)。你不需要检测任何东西;这就是 stdatomic 的用途。请记住:当前标准版本是 C11,不再是 C90!如果没有正确对齐,64 位机器可能仍然执行关于多进程的非原子读/写。
      • @Olaf:在某些情况下,'volatile' 实际上是可选的,但是是的,最好使用它。至于“stdatomic”库,如果 64 位类型在目标拱上没有本机原子指令,atomic_store() 会做什么?会不会是编译错误?还是编译器会使用某种锁定机制?
      • volatile 始终是可选的;编译器甚至可能会忽略它(但我不知道实际上有什么)。但是,某些应用程序实际上确实需要它。对于这个问题,它是强制性的,但还不够;还必须满足其他约束:读/写的原子性,可能是栅栏/障碍。关于标准原子:你应该看看实际的实现。根据 CPU 的不同,它使用 CAS 或独占加载/存储指令,具体取决于函数指定的内存模型。对于 ARM-V7(AMR) 和 (afaik) x86,使用后者。 ...
      • ... 这基于目标更改时重新启动操作。其他架构可能会使用锁定指令来锁定总线以防止其他主机使用。有些可能不需要什么特别的,因为它们只有一个 CPU,而我们只有一条指令,例如添加。对于对齐内存的简单读/写,甚至可能没有特殊指令,而是之前或之后的内存屏障(再次:取决于给定的内存模型)。哦,如果根本无法保证,相应的功能根本不可用,所以:是的,你会从编译器中得到一个错误。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多