非保护危害在线程环境中共享变量

时间:2010-11-10 21:58:21

标签: multithreading thread-safety

我试图理解在线程(或共享内存)环境中不锁定共享变量的危险。很容易争辩说,如果你对一个变量进行两次或多次依赖操作,首先要保持一些锁是很重要的。典型的例子是递增操作,它首先在添加一个并写回之前读取当前值。

但是,如果你只有一个作家(和许多读者)并且写入不依赖于先前的值,那该怎么办呢?所以我有一个线程每秒存储一次时间戳偏移量。偏移量保持当地时间与其他时基之间的差异。很多读者使用这个偏移来为时间戳记事件,并且每次获得读锁定都有点贵。在这种情况下,我不关心读者是否在写入之前或之后获取值,只要读者没有得到垃圾(这是一个从未设置的偏移量)。

假设变量是32位整数。是否有可能在写入过程中获取变量的垃圾读取?或者正在写一个32位整数的原子操作?它会取决于Os还是硬件?关于32位系统的64位整数怎么样?

共享内存而不是线程怎么样?

6 个答案:

答案 0 :(得分:3)

在32位系统上写入64位整数不是原子的,如果不锁定,可能会有不正确的数据。

例如,如果您的整数是

0x00000000 0xFFFFFFFF

并且您将按顺序编写下一个int,您想写:

0x00000001 0x00000000

但是如果你在写入一个int之后和另一个之前读取了值,那么你可以阅读

0x00000000 0x00000000

0x00000001 0xFFFFFFFF

与正确的值完全不同。

如果你想在没有锁的情况下工作,你必须非常确定OS / CPU /编译器组合的原子操作是什么。

答案 1 :(得分:2)

除了上述评论之外,请注意稍微更一般的设置中的寄存器库。您可能最终只更新cpu寄存器而不是立即将其写回主存储器。或者在内存中的原始值更新时使用缓存寄存器副本的另一种方式。有些语言使用volatile关键字将变量标记为“read-always-and-never-local-register-cache”。

您语言的内存模型非常重要。它准确描述了在多个线程之间共享给定值的条件。这是您正在执行的CPU体系结构的规则,或者由运行语言的虚拟机确定。例如,Java有一个单独的内存模型,您可以查看它以确定预期的内容。

答案 2 :(得分:1)

如果8位,16位或32位读/写符合其大小(在486及更高版本上)并且未对齐但在高速缓存行(在P6及更高版本中),则保证8位,16位或32位读/写是原子的。大多数编译器都会保证堆栈(本地,假设C / C ++)变量是一致的。

如果对齐(在Pentium和更高版本上),64位读/写保证是原子的,但是,这依赖于编译器生成单个指令(例如,从FPU弹出64位浮点数)或使用MMX)。我希望大多数编译器都会使用两个32位访问来实现兼容性,尽管可以检查(反汇编)并且可能会强制执行不同的处理。

下一个问题是缓存和内存防护。但是,忽略这些的效果是某些线程可能会看到旧值,即使它已被更新。该值不会无效,只是过时(可能是微秒)。如果这对你的应用至关重要,你将不得不深入挖掘,但我怀疑它是。

(资料来源:Intel Software Developer Manual Volume 3A

答案 3 :(得分:0)

这在很大程度上取决于硬件和你如何与它交谈。如果您正在编写汇编程序,您将确切地知道您获得了什么,因为处理器手册将告诉您哪些操作是原子操作以及在什么条件下。例如,在Intel Pentium中,如果地址对齐,则32位读取是原子的,但不是。

如果您正在处理上述任何级别,那将取决于最终如何将其转换为机器代码。是编译器,解释器或虚拟机。

答案 4 :(得分:0)

您运行的平台决定了原子读/写的大小。通常,32位(寄存器)平台仅支持32位原子操作。因此,如果您编写的是32位以上,则可能必须使用其他一些机制来协调对该共享数据的访问。

一种机制是对实际数据进行双倍或三倍缓冲,并使用共享索引来确定“最新”版本:

write(blah)
{
    new_index= ...; // find a free entry in the global_data array.
    global_data[new_index]= blah;
    WriteBarrier(); // write-release
    global_index= new_index;
}

read()
{
    read_index= global_index;
    ReadBarrier(); // read-acquire
    return global_data[read_index];
}

您需要内存障碍,以确保在阅读global_data[...]之前不会从global_index开始阅读,并且在您写信至{global_index之后才写入global_data[...] 1}}。

这有点糟糕,因为你也可以通过抢占遇到ABA问题,所以不要直接使用它。

答案 5 :(得分:0)

平台通常提供原始读/写访问(在硬件级别强制执行)到原始值(32位或64位,如示例所示) - 请参阅the Interlocked* APIs on Windows

这可以避免对线程安全变量或成员访问使用较重的权重锁,但不应与同一实例或成员上的其他类型的锁混淆。换句话说,请勿使用Mutex在一个位置调解访问权限,并使用Interlocked*在另一个位置修改或阅读。