在我的《可见性、原子性、有序性》博文中,说到可见性、原子性与有序性是高并发编程的一切问题的源泉。那么我们是不是可以关闭编译器与CPU的优化呢,那样的话就不会有可见性与有序性的问题了,关闭优化时可以解决问题,但是同时也会导致程序的性能低下。但是哪些情况下需要关闭优化,只有我们程序员自己知道,那么我们能不能按需关闭优化呢?本文就谈谈C语言如何解决可见性与有序性的问题,对于其他语言来说也有触类旁通的作用。
可见性
由于多核CPU的每个核心都有各自的缓存,当共享变量在多个线程中使用的时候,由于每个线程可能运行在不同的核心上,当一个线程更新了缓存的共享变量时,另一个缓存的共享变量可能没有同步更新,从而导致另一个线程没有读到最新的修改。C语言通过引入volatile关键字告诉CPU禁用缓存,每次读写操作都直接操作内存,从而解决可见性的问题。
static volatile int a = 0;
void func1()
{
a = 10;
}
void func2()
{
int b = 1;
}
对于如上的程序,如果func1再线程1先执行,func2再线程2再执行,那么线程2可以获取到a的最新的值。volatile还可以阻止CPU如下的优化:
static int foo;
void bar(void) {
foo = 0;
while (foo != 255)
;
}
如果没有volatile,由于编译器认为foo只再当前线程中使用,可能会被优化成如下:
void bar_optimized(void) {
foo = 0;
while (true)
;
}
从而导致程序没有按预期的运行。
有序性
由于编译器的优化与CPU的乱序执行,如下的函数
void func2()
{
int a = 2;
int b = 1;
int c = a;
}
由于a = 2与b = 1两条语句没有上下文的关系,故编译器编译后可能优化成b = 1、a = 2。当然CPU优化也同理,如b已经再缓存中,但是a不再,那么CPU就必须从内存加载b到缓存中,由于加载内存的过程中可以执行几十条CPU指令了,现代CPU的流水线技术使得加载内存到缓存的过程中可以执行其他指令,加载的过程中b已经更新到缓存了,从而导致CPU的乱序执行。
为此如下的代码
static volatile int a = 0;
static volatile int b = 0;
void func1()
{
a = 10;
b = 11;
}
void func2()
{
if(b == 11)
{
int c = a;
}
}
func1与func2分别再线程1与线程2执行,由于CPU优化后(volatile只能阻止编译器优化顺序)func1里的b可能先赋值,func2的判断条件满足时,a的值可能为0。从而导致执行结果与我们预期的不一致。
为此Linux提供了如下的宏定义
#ifdef CONFIG_X86_32
/*
* Some non-Intel clones support out of order store. wmb() ceases to be a
* nop for these.
*/
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
#else
#define mb() asm volatile("mfence":::"memory")//串行化发生在mb之前的写操作但是不影响读操作
#define rmb() asm volatile("lfence":::"memory")//串行化发生在rmb之前的读操作但是不影响写操作
#define wmb() asm volatile("sfence" ::: "memory")//串行化发生在wmb之前的读写操作
#endif
他们约束了CPU,也自然约束了编译器,他们通常用于C/C++的无锁编程。如下的代码如果改成
static volatile int a = 0;
static volatile int b = 0;
void func1()
{
a = 10;
wmb();
b = 11;
}
void func2()
{
if(b == 11)
{
rmb()
int c = a;//此时a一定是10
}
}
就可以按预期工作了。linux还提供了smp_**()函数,如下所示
#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#endif
CONFIG_SMP是用来控制单处理器与多处理器的宏。如果是UP(uniprocessor)系统就是如下语义:
#define barrier() __asm__ __volatile__("": : :"memory")
barrier()用来告诉编译器,串行化发生在barrier之前的写读操作。这意味着barrier后的读操作需要从内存里面读取,不能从缓存读取。