Dekker algorithm是一种著名的并发编程的算法,Dekker算法的核心部分是一组对称的代码来访问一组共享变量,使得两个线程不可能同时进入临界区(只要cpu内存模型是遵循顺序一致性的),从而达到线程同步的目的。以下是该算法的一种实现:
static volatile int flag1 = 0; static volatile int flag2 = 0; static volatile int turn = 1; static volatile int gSharedCounter = 0; void dekker1( ) { flag1 = 1; turn = 2; while((flag2 == 1) && (turn == 2)) ; // Critical section gSharedCounter++; // Let the other task run flag1 = 0; } void dekker2(void) { flag2 = 1; turn = 1; while((flag1 == 1) && (turn == 1)) ; // critical section gSharedCounter++; // leave critical section flag2 = 0; }
该实现的关键在于while((flag2 == 1) && (turn == 2))与while((flag1 == 1) && (turn == 1))永远不可能同时成立,从而可以互斥的访问临界区,并且由于条件不可能同时成立,也不会导致死锁。以下面的例子进行测试:
int gLoopCount; void *task1(void *arg) { int i; printf("Starting task1n"); for(i=gLoopCount;i>0;i--) { dekker1(); } } void *task2(void *arg) { int i; printf("Starting task2n"); for(i=gLoopCount;i>0;i--) { dekker2(); } }
在单核处理器下,以多线程的方式执行这段代码,无论运行多少次,程序也不会出错。因为单核CPU的内存模型是遵循顺序一致性(Sequential Consistency)的,Sequential Consistency模型(后面简称SC),简单说它其实就是我们印象中多线程程序应该有的执行顺序。但是,SC最大的问题是性能太低了,因为CPU/编译器完全没有必要严格按代码规定的顺序(program order)来执行每一条指令。学过体系结构的同学应该知道不管是编译器也好CPU也好,他们最擅长做的事情就是帮你做乱序优化。在串行时代这些乱序优化对程序员来说都是透明的,封装好了的,你不用关心它们到底给你乱序成啥样了,因为它们会保证优化后的程序的运行结果跟你写程序时预期的结果是一模一样的。但是进入多核时代之后,CPU和编译器还会继续做那些串行时代的优化,更重要的是这些优化还会打破你多线程程序的SC模型语义,从而使得多线程程序的实际运行结果与我们所期待的运行结果不一致!
拿X86来说,它的多核内存模型没有严格执行SC,即属于weak ordering(或者叫relax ordering?)。它唯一允许的乱序优化是可以把对不同地址的load操作提到store之前去(即把store x->load y乱序优化成load y -> store x)。而store x -> store y、load x -> load y,以及load y -> store x不允许交换执行顺序。
因此,对于弱内存一致性的机器来说,该算法有可能会失效,因为对于按什么顺序来更新flag1和flag2是没有限制的,特别是不能保证在dekker1中对flag2的读操作发生在dekker2对flag1和turn的写操作之后。
参考:剖析为什么在多核多线程程序中要慎用volatile关键字?