假设在C程序中我有一个在32位机器上运行的P线程,int MAX
- 一个共享的32位整数
每个线程都可以读/写MAX。
要求:线程读取的值不应该被破坏,例如,前16位和后16位不同步
问题:我是否需要锁来保护读写?或者我可以安全地忽略锁定,因为保证LOAD / SAVE汇编指令原子地发生吗?
答案 0 :(得分:3)
当int正确对齐时,读取和写入是原子的。它无法跨越缓存行的末尾。高速缓存行是64字节。大多数编译器都会确保对齐得到处理,但是可以用结构打包编译指示覆盖它。
是的,当线程执行读 - 修改 - 写操作时,您需要一个锁来保护该值。你可以从InterlockedXxxx获得一个便宜的,也许。
答案 1 :(得分:3)
这里的问题是read-modify-write。
假设你有两个独立的物理核心(它们可以在同一个物理包中)。他们都读了。第一个核心修改 - 也就是说,增加当前保存在寄存器中的值。此时,修改开始传播到缓存 - 但同时第二个核心也修改了值,并开始将缓存传播到缓存。
您丢失了其中一项更新。
缓存一致性协议不处理多个并发写入的情况。没有什么能够导致一个核心等待它的写入,因为另一个核心也是 - 当前写入 - ;因为这些信息在核心之间根本不公开。他们 - 不能这样做。
他们会处理多个连续写入,例如在核心外部总线引脚上看到变化之后(例如,成为公共知识,而不是核心内部)。
另一个问题是指令重新排序。这些线程 - 如果它们在不同的内核上运行,它们的指令重新排序将不会注意其他线程正在做什么;只针对该线程正在做的事情。
想象一个线程要写入值然后设置一个标志。另一个线程将看到引发的标志,然后读取该值。这些线程,如果在单独的核心上,将仅针对自己重新排序其指令流 - 核心不会考虑其他线程,因为它们不能 - 他们不了解它们。
因此,在第一个线程中,可以重新排序标志设置,以便在写入值之前发生 - 毕竟,对于只是那个线程,订购很好。这两个变量完全断开。没有排序依赖。依赖存在于另一个在不同核心上的线程中,因此我们的核心无法知道它。
另一个线程当然会看到引发并读取的标志,即使写入实际上还没有发生。
答案 2 :(得分:2)
不,字大小的加载和存储是原子的。但是,您希望执行添加或其他算法,因为它们可能使用读取和写入,因此需要锁定。
答案 3 :(得分:2)
不同的架构/ CPU具有不同的原子性保证。 C旨在是可移植的,因此您不应对任何特定体系结构/ CPU所做的原子性保证做出任何假设。这意味着即使对于原子读/写,您也应该使用某种抽象(例如,为每个不同的体系结构提供必要的原子操作的库)。
据我所知(这不是很多 - 我的经验大部分只是80x86),对于大多数架构来说,对小于某个最小大小的对齐地址的读写通常保证是原子的(其中“某些最小大小”可能是通用寄存器的大小,高速缓存行的大小或其他内容。
这不包括修改(例如,读取,修改,然后写入地址的指令/操作)。对于“int MAX”变量(与“const int MAX = 1234”相反),我假设您想要执行类似“if(foo> MAX)MAX = foo”的操作;并且你需要一个更强大的原子操作(例如,如果比较为假,则可以在循环中重新进行原子“比较和交换”。)
另外,不要忘记将变量声明为“volatile”。
答案 4 :(得分:2)
大多数(全部?)现代CPU都有特殊指令,可以保证对这类变量的原子访问。 C现在具有这些指令的标准化接口。不幸的是,大多数编译器还没有完全实现这一点。
您应该使用扩展程序。例如,gcc有一个whole bunch of them。如果你在网络上看起来很棒,你也应该很容易找到可以与其他编译器一起使用的内联汇编指令的实现。
答案 5 :(得分:0)
我不确定是否有任何明确的保证,但只要该值存储在正确对齐的存储器位置(即存储器地址是4的倍数),那么在实践中你会没事的;内存只能读取/写入至少大小的块。