读取一个没有锁定同时修改的整数变量是否安全?

时间:2009-08-29 09:48:35

标签: c++ multithreading concurrency

假设我在类中有一个整数变量,并且该变量可能被其他线程同时修改。写入受互斥锁保护。我是否也需要保护读取?我听说有一些硬件架构,如果一个线程修改一个变量,另一个线程读取它,那么读取结果将是垃圾;在这种情况下,我需要保护读取。我从未见过这样的架构。

这个问题假设单个事务只包含更新单个整数变量,因此我并不担心可能也涉及事务的任何其他变量的状态。

12 个答案:

答案 0 :(得分:34)

原子阅读
如前所述,它依赖于平台。在x86上,该值必须在4字节边界上对齐。通常,对于大多数平台,读取必须在单个CPU指令中执行。

优化器缓存
优化器不知道您正在读取由不同线程修改的值。声明值volatile有助于:优化器将为每次访问发出内存读/写,而不是试图将值缓存在寄存器中。

CPU缓存
尽管如此,您可能会读到一个陈旧的值,因为在现代架构中,您有多个具有单独缓存的核心,而这些核心并未自动保持同步。您需要一个读取内存屏障,通常是特定于平台的指令。

在Wintel上,线程同步功能会自动添加完整的内存屏障,或者您可以使用InterlockedXxxx函数。

MSDN:Memory and Synchronization issuesMemoryBarrier

[编辑]也请看drhirsch的评论。

答案 1 :(得分:14)

你问一个关于读变量的问题,然后你谈到更新变量,这意味着读 - 修改 - 写操作。

假设你真的是指前者,那么如果它是原子操作,则读取是安全的。对于几乎所有的体系结构,这对于整数来说都是正确的。

有一些(和罕见的)例外:

  • 读取未对齐,例如访问奇数地址处的4字节int。通常你需要强制编译器使用特殊属性来做一些错位。
  • int的大小大于指令的自然大小,例如在8位架构上使用16位整数。
  • 某些架构的人工有限的总线宽度。我只知道很老的和过时的,比如386sx或68008。

答案 2 :(得分:8)

在这种情况下,我建议不要依赖任何编译器或架构 每当你有一群读者和作家(而不仅仅是读者或只是作家)时,你最好将它们全部同步。想象一下,你的代码运行的是一个人的心脏,你真的不希望它读错了值,你肯定不希望你所在城市的电厂变得“沸腾”,因为有人决定不使用那个互斥锁。从长远来看,让自己保持夜间睡眠,同步他们 如果你只有一个线程读取 - 你只使用那个互斥锁是好的,但是如果你计划多个读者和多个编写者,你需要一个复杂的代码来同步它。我还没有看到一个很好的读/写锁实现,它也是“公平的”。

答案 3 :(得分:5)

想象一下,您正在一个线程中读取变量,该线程在读取时被中断,并且该变量由写入线程更改。现在读取线程恢复后读取整数的值是多少?

除非读取变量是原子操作,在这种情况下只需要一条(汇编)指令,否则无法确保上述情况不会发生。 (变量可以写入内存,检索值将需要多个指令)

共识是你应该单独封装/锁定所有写入,而读取可以与(仅)其他读取同时执行

答案 4 :(得分:3)

  

假设我在类中有一个整数变量,并且该变量可能被其他线程同时修改。写入受互斥锁保护。我是否也需要保护读取?我听说有一些硬件架构,如果一个线程修改一个变量,另一个线程读取它,那么读取结果将是垃圾;在这种情况下,我需要保护读取。我从未见过这样的架构。

在一般情况下,这可能是每个架构。每个体系结构都存在与写入同时读取将导致垃圾的情况。 但是,几乎每个架构都有这个规则的例外。

通常以字面方式读取和写入字大小的变量,因此在读取时不需要同步。正确的值将作为单个操作原子写入,并且即使另一个线程正在写入,线程也会将当前值读取为单个原子操作。因此,对于整数,您在大多数架构上都是安全的。有些人会将这种保证扩展到其他几种尺寸,但这显然与硬件有关。

对于非字大小的变量,读取和写入通常都是非原子的,并且必须通过其他方式进行同步。

答案 5 :(得分:2)

如果在写新内容时不使用此变量的主要值,则:

您可以在不使用互斥锁的情况下读取和写入整数变量。这是因为整数是32位架构中的基类型,并且每次修改/读取值都是通过一次操作完成的。

但是,如果你不喜欢增量:

myvar++;

然后你需要使用互斥锁,因为这个结构扩展到myvar = myvar + 1,在读取myvar和增量myvar之间,可以修改myvar。在这种情况下,你会得到不好的价值。

答案 6 :(得分:2)

虽然在没有同步的情况下读取32位系统上的内容可能是安全的。我不会冒险。虽然多个并发读取不是问题,但我不喜欢写入与读取同时发生。

我建议将读取放在Critical Section中,然后在多个内核上对应用程序进行压力测试,看看是否会导致过多的争用。找到并发错误是我想避免的噩梦。如果将来有人决定将int更改为long long或double,那么会发生什么呢?所以他们可以保留更大的数字?

如果你有一个很好的线程库,如boost.thread或zthread,那么你应该有读/写锁。这些对于您的情况是理想的,因为它们允许多次读取,同时保护写入。

答案 7 :(得分:1)

这可能发生在使用16位整数的8位系统上。

如果你想避免锁定,你可以在适当的情况下多次阅读,直到你得到两个相等的连续值。例如,我使用这种方法在32位嵌入式目标上读取64位时钟,其中时钟节拍被实现为中断例程。在这种情况下,读取三次就足够了,因为时钟只能在读取程序运行的短时间内打一次。

答案 8 :(得分:0)

对具有并发性的变量进行读/写操作必须受关键部分(不是互斥锁)的保护。除非你想浪费一整天的时间进行调试。

我认为关键部分是特定于平台的。在Win32上,临界区非常有效:当没有发生互锁时,进入临界区几乎是免费的,不会影响整体性能。当发生互锁时,它比mutex更有效,因为它在挂起线程之前实现了一系列检查。

答案 9 :(得分:0)

通常,每个机器指令在执行时都会通过几个硬件阶段。由于大多数当前的CPU是多核或超线程的,这意味着读取变量可能会启动它通过指令管道移动,但它不会阻止另一个CPU核心或超线程同时执行存储指令到同一个地址。两个同时执行的指令(读取和存储)可能是“交叉路径”,这意味着读取将在存储新值之前接收旧值。
要恢复:您确实需要使用互斥锁进行读写操作。

答案 10 :(得分:-1)

取决于您的平台。大多数现代平台为整数提供原子操作:Windows具有InterlockedIncrement,InterlockedDecrement,InterlockedCompareExchange等。这些操作通常由底层硬件(读取:CPU)支持,并且它们通常比使用关键部分或其他同步机制便宜。 / p>

请参阅MSDN:InterlockedCompareExchange

我相信Linux(和现代Unix变种)在pthreads包中支持类似的操作,但我并不认为自己是那里的专家。

答案 11 :(得分:-3)

如果变量用volatile关键字标记,则读/写变为原子,但就编译器的作用及其行为方式而言,这有许多其他含义,不应仅用于此目的。

在盲目开始使用它之前,请阅读volatile的内容:http://msdn.microsoft.com/en-us/library/12a04hfd(VS.80).aspx