这个问题是并发编程困难的教科书示例。一个真正彻底的解释可以写满整本书,以及许多质量参差不齐的文章。
但我们可以稍微总结一下。全局变量位于所有线程可见的内存空间中。(另一种选择是线程本地存储,只有一个线程可以看到。)所以你会期望如果你有一个全局变量G,并且线程A将值x写入它,那么线程B将在读取该变量时看到x稍后的。总的来说,这是真的——最终。有趣的部分是“最终”之前发生的事情。
棘手的最大来源是内存一致性和内存连贯性。
连贯性描述了当线程A写入G并且线程B几乎同时尝试读取它时会发生什么。假设线程A和B位于不同的处理器上(为简单起见,我们也称它们为 A 和 B)。当A写入一个变量时,它和线程B看到的内存之间有很多电路。首先,A可能会写入自己的数据缓存。它会将该值存储一段时间,然后再将其写回主存储器。将缓存刷新到主内存也需要时间:有一个必须在电线、电容器和晶体管上来回传输的信号数量,以及高速缓存和主存储器单元之间的复杂对话。同时,B有自己的缓存。当主存发生变化时,B可能不会立即看到它们——至少在它从该行重新填充其缓存之前是这样。等等。总而言之,在线程A的更改对B可见之前可能需要很多微秒。
一致性描述了当A写入变量G然后变量H时会发生什么。如果它读回这些变量,它将看到按该顺序发生的写入。但是线程B可能会以不同的顺序看到它们,这取决于H是否首先从缓存刷新回主 RAM。如果A和B同时(通过挂钟)写入G ,然后尝试从它读回,会发生什么?他们会看到什么价值?
在许多具有内存屏障操作的处理器上强制执行一致性和一致性。例如,PowerPC 有一个同步操作码,上面写着“保证任何线程对主内存进行的任何写入,在此同步操作之后对任何读取都是可见的”。(基本上,它通过针对主 RAM 重新检查每个缓存行来做到这一点。)如果您提前警告英特尔架构“此操作触及同步内存” ,英特尔架构会在某种程度上自动执行此操作。
然后你有编译器重新排序的问题。这是代码的地方
int foo( int *e, int *f, int *g, int *h)
{
*e = *g;
*f = *h;
// <-- another thread could theoretically write to g and h here
return *g + *h ;
}
可以由编译器在内部转换为更像
int bar( int *e, int *f, int *g, int *h)
{
int b = *h;
int a = *g;
*f = b ;
int result = a + b;
*e = a ;
return result;
}
如果另一个线程在上面给出的位置执行写入,这可能会给你一个完全不同的结果!另外,请注意写入是如何以不同的顺序发生在bar
. 这是volatile应该解决的问题——它阻止编译器将 的值存储*g
在本地,而是强制它每次看到 时都从内存中重新加载该值*g
。
如您所见,这不足以在许多处理器之间强制执行内存一致性和一致性。它真的是为你有一个处理器试图从内存映射硬件读取的情况而发明的——比如一个串行端口,你想每隔n微秒查看一次内存中的一个位置,以查看当前在线路上的值. (这就是他们发明 C 时 I/O 的实际工作方式。)
该怎么办?好吧,就像我说的,关于这个主题有整本书。但简短的回答是,您可能希望使用您的操作系统/运行时平台为同步内存提供的设施。
例如,Windows 提供了互锁的内存访问 API,为您提供了一种在线程A和B之间进行内存通信的清晰方法。GCC 试图公开一些类似的功能。英特尔的线程构建块为您提供了一个很好的 x86/x64 平台接口,C++11 线程支持库也提供了一些工具。