C语言如何解决可见性、有序性问题

在我的《可见性、原子性、有序性》博文中,说到可见性、原子性与有序性是高并发编程的一切问题的源泉。那么我们是不是可以关闭编译器与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后的读操作需要从内存里面读取,不能从缓存读取。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值