我试图理解在线程(或共享内存)环境中不锁定共享变量的危险。很容易争辩说,如果你对一个变量进行两次或多次依赖操作,首先要保持一些锁是很重要的。典型的例子是递增操作,它首先在添加一个并写回之前读取当前值。
但是,如果你只有一个作家(和许多读者)并且写入不依赖于先前的值,那该怎么办呢?所以我有一个线程每秒存储一次时间戳偏移量。偏移量保持当地时间与其他时基之间的差异。很多读者使用这个偏移来为时间戳记事件,并且每次获得读锁定都有点贵。在这种情况下,我不关心读者是否在写入之前或之后获取值,只要读者没有得到垃圾(这是一个从未设置的偏移量)。
假设变量是32位整数。是否有可能在写入过程中获取变量的垃圾读取?或者正在写一个32位整数的原子操作?它会取决于Os还是硬件?关于32位系统的64位整数怎么样?
共享内存而不是线程怎么样?
答案 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位访问来实现兼容性,尽管可以检查(反汇编)并且可能会强制执行不同的处理。
下一个问题是缓存和内存防护。但是,忽略这些的效果是某些线程可能会看到旧值,即使它已被更新。该值不会无效,只是过时(可能是微秒)。如果这对你的应用至关重要,你将不得不深入挖掘,但我怀疑它是。
答案 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*
在另一个位置修改或阅读。