在C语言中,改变指针是否被视为原子操作?

34

如果我有一个多线程程序,通过引用读取缓存类型的内存。主线程可以在不影响其他线程读取意外值的情况下更改该指针吗?

在我看来,如果更改是原子性的,其他线程将读取旧值或新值;永远不会读取随机内存(或空指针),对吗?

我知道我应该始终使用同步方法,但我还是很好奇。

指针变化是否是原子性的?

更新:我的平台是64位Linux(2.6.29),虽然我也想要跨平台的答案:)

7个回答

26

正如其他人所提到的,C语言中没有任何保证,这取决于你的平台。

在大多数现代桌面平台上,对一个字大小的、对齐的位置进行读/写将是原子性的。但这并不能解决你的问题,因为处理器和编译器会重新排序读取和写入的操作。

例如,以下代码是错误的:

线程A:

DoWork();
workDone = 1;

线程 B:

while(workDone != 0);

ReceiveResultsOfWork();
尽管对workDone的写入是原子性的,但在许多系统上,处理器不能保证在DoWork()通过写入可见之前,其他处理器将写入workDone。编译器也可能自由地将对workDone的写入重新排序到DoWork()调用之前。在这两种情况下,ReceiveResultsOfWork()可能会开始使用不完整的数据。
根据您的平台,您可能需要插入内存屏障等来确保正确的顺序。这可能非常棘手。
或者只需使用锁。更简单,更容易验证为正确,并且在大多数情况下性能足够。

3
如果编译器可以证明DoWork没有以任何定义的方式访问workDone,那么它可以重新排序。如果DoWork足够小并且在同一翻译单元中,编译器决定内联它,这种情况可能会发生。 - derobert
2
虽然它是C++的关键字,但大多数C编译器都支持volatile修饰符,这将防止编译器重新排序对变量的写入,并几乎保证内存通过缓存进行写入。我说几乎是因为在C中没有什么是保证的,但这是每个主要的C编译器的惯例。 - Jeff Mc
2
我不相信仅靠volatile就能防止重排序,它只是强制编译器在读取时重新获取并且不优化写入。如果可以证明DoWork()不修改workDone()并且不访问任何其他的volatile,即使workDone是volatile,编译器仍然可以自由地重新排序workDone = 1。当然,编译器可以为volatile关键字添加更多含义,我听说过一些平台将volatile扩展为具有重排序的含义。但我不认为标准保证了这一点。 - Michael
3
@Don:内存屏障不是为了防止编译器重排序而设计的,它们是为了防止 CPU 中的重排序。 - Ben Voigt
1
@Don:编译器必须在没有信号处理程序的单线程应用程序中保留表面顺序(信号处理程序可能比线程更棘手...)。一旦你有了线程,你需要查看你的线程库;请参阅pthread_barrier_*和类似函数。 - tc.
显示剩余9条评论

13

C语言对于任何操作是否具有原子性并没有明确规定。我曾经使用过8位总线和16位指针的微控制器,这些系统中的任何指针操作都可能是非原子的。我记得 Intel 386(其中一些有16位总线)也引起了类似的担忧。同样,我可以想象一些具有64位CPU但只有32位数据总线的系统,这可能会涉及到指针操作的非原子性问题。(我还没有检查是否有这样的系统存在。)

编辑:Michael's answer 值得一读。总线大小与指针大小并不是关于原子性的唯一考虑因素;这只是我想到的第一个反例。


1
@ojblass:这种事情在旧的架构中发生过;即使是新的架构也可能会发生。(请看我的更新评论。) - Dan Breslau
1
@ojblass,当使用C/C++编程时,我喜欢采取一种立场:除非标准和编译器文档说明不可能,否则一切皆有可能。这是保证安全的唯一方法 :) - JaredPar
1
指针的读写可能从总线的角度来看不是原子性的,但如果它是单个指令,那么从CPU的角度来看它很可能是原子性的。奇怪的架构可能不会进行任何形式的多处理,所以通常情况下都没问题。通常而言,希望一切顺利。 - tc.
@tc:单个指令的行为仍然与平台有关。我知道有一些CPU(尽管我不记得是哪些)可以在指令执行过程中处理中断。当发生这种情况时,所有赌注都无效。 - Dan Breslau

5
你没有提到具体的平台。因此,更准确的问题应该是:

指针变化是否保证原子性?

这个区别很重要,因为不同的C/C++实现在这个行为上可能会有所不同。一个特定的平台可以保证原子赋值并仍符合标准。

至于在C/C++中是否保证了整体的原子性,答案是否定的。C标准没有作出任何这方面的保证。保证指针赋值是原子的唯一方法是使用特定于平台的机制来保证赋值的原子性。例如,Win32中的Interlocked方法将提供这种保证。

你在哪个平台上工作?


4

没错,C语言规范并没有要求指针赋值必须是原子操作,因此你不能保证指针赋值的原子性。

实际情况则取决于你使用的平台、编译器以及可能是你编写程序当天星座的影响。


2
这不是推卸责任,而是正确的答案。如果你正在使用并发,你 需要 把它做对;否则你会浪费更多时间去复制一个万亿分之一的Heisenbug。 - Alex Feinman

3

'normal'指针修改不能保证是原子操作。

检查“比较并交换”(CAS)和其他原子操作,这不是C标准,但大多数编译器都可以访问处理器原语。在GNU gcc的情况下,有几个内置函数可用。


1

标准保证的唯一类型是sig_atomic_t。

正如其他答案所示,当针对通用x86架构时,它很可能是可以的,但在更多“专业”硬件上非常危险。

如果你真的很想知道,你可以将sizeof(sig_atomic_t)与sizeof(int*)进行比较,并查看它们在目标系统上的大小。


1

这其实是一个相当复杂的问题。我曾经问过一个类似的问题,并阅读了所有指向我的内容。我学到了很多关于现代架构中缓存如何工作的知识,但没有找到什么是最终确定的答案。正如其他人所说,如果总线宽度小于指针位宽,你可能会遇到麻烦。特别是如果数据跨越了缓存行边界。

一个谨慎的架构将使用锁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接