最近一个项目中用到了peterson算法来作临界区的保护,简简单单的十几行代码,就能实现两个线程对临界区的无锁访问,确实很精炼。可是在这不是来分析peterson算法的,在实际应用中发现peterson算法并不能对临界区进行互斥访问,也就是说两个线程仍是有可能同时进入临界区。可是按照代码的分析,明明能够实现互斥访问的呀,这是怎么回事呢?ios
首先用一个测试程序来检验一下。临界区是对一个全局变量的自加一运算,两个线程各加一百万次,最后结果应该是两百万。因为自加一运算不是原子的,若是两个线程同时进入临界区,最后的结果就会少于两百万。算法
#include <iostream> #include <pthread.h>
using namespace std; static volatile bool flag[2] = {false, false}; static volatile int turn = 0; static volatile int gCount = 0; void procedure0() { flag[0] = true; turn = 1; while (flag[1] && (turn == 1)); gCount++; flag[0] = false; } void procedure1() { flag[1] = true; turn = 0; while (flag[0] && (turn == 0)); gCount++; flag[1] = false; } void* ThreadFunc0(void* args) { int i; for (i = 0; i<1000000; i++) procedure0(); return NULL; } void* ThreadFunc1(void* args) { int i; for (i = 0; i<1000000; i++) procedure1(); return NULL; } int main() { pthread_t pid0, pid1; if (pthread_create(&pid0, 0, &ThreadFunc0, NULL)) { cout << "Create thread0 failed." << endl; return 1; } if (pthread_create(&pid1, 0, &ThreadFunc1, NULL)) { cout << "Create thread1 failed." << endl; return 1; } pthread_join(pid0, NULL); pthread_join(pid1, NULL); cout << gCount << endl; if (gCount == 2000000) cout << "Success" << endl; else cout << "Fail" << endl; return 0; }
x86平台peterson锁失效 arm平台peterson锁失效ide
这个测试程序在Linux上用gcc编译,不管用O0,O1,O2编译选项,我试过x86平台,Arm平台,结果都有可能小于两百万,也就是这样实现的peterson锁不能阻止两个线程同时进入临界区。缘由在于现代的编译器和多核CPU由于优化代码的缘由,最擅长的事情就是指令乱序执行。编译器作的是静态乱序优化,CPU作的是动态乱序优化。简单来讲,就是指令最终在CPU的执行顺序和咱们在程序中写的顺序多是截然不同的。固然这种乱序执行是要在保证最终执行结果正确的前提下的,大多数状况下都不会引发问题,咱们对指令的乱序执行也毫无感知。可是在一些特殊的状况下,好比peterson算法里,乱序优化可能会引发问题。函数
一般状况下,乱序优化均可以把对不一样地址的load操做提到store以前去,我想这是由于load操做若是cache命中的话,要比store快不少。以线程0为例,看这3行。测试
flag[0] = true; turn = 1; while (flag[1] && (turn == 1));
前两行是store,第三行是load。可是对同一变量turn的store再load,乱序优化是不可能对他们交换顺序的。可是flag[0]和flag[1]是不一样的变量,先store后load就可能被乱序优化成先load flag[1],再store flag[0]。假设两个线程都已退出临界区,准备再次进入,此时flag[0]和flag[1]都是false。按乱序执行先load,两个线程都会有while条件为假,则同时均可以进入了临界区,互斥失效!这就是在有些状况下要保持代码的顺序一致性的重要。优化
这个问题怎么解决呢?也很简单,就是使用内存栅栏(memory barrier)。顾名思义,他就像个栅栏同样摆在两段代码之间,阻止编译器或者CPU在这两段代码之间进行乱序优化。在x86平台上,阻止编译器的静态乱序优化的汇编代码是spa
asm volatile("" ::: "memory");
可是它不能阻止CPU运行时的乱序优化。在这里咱们须要的不单单是阻止静态乱序,还要阻止动态乱序。x86的动态内存栅栏汇编命令有三条,分别是lfence,sfence和mfence,分别表示load栅栏,store栅栏和读写栅栏。也就是lfence只能保证lfence以前的读命令不和它以后的读命令发生乱序。sfence保证sfence以前的写命令不和它以后的写命令发生乱序。mfence保证了它先后的读写命令不发生乱序。这里咱们须要用mfence,不过实际上我是了sfence也是能够的,可是lfence不行。线程
flag[0] = true;
turn = 1; asm("mfence"); while (flag[1] && (turn == 1));
在中间插入一行内存栅栏指令,这样peterson测试程序执行才是彻底正确的。code
在arm平台,相应的内存栅栏指令有三条,dmb(data memroy barrier),dsb(date synchronization barrier)和isb(instruction synchronization barrier)。dmb保证在dmb以前的内存访问指令在它以后的内存访问指令以前完成,也就是阻止了乱序。dsb更严格一些,保证在dsb完成以前,全部它以前的指令都执行完成。isb最严格,它会清空处理器的流水线,固然就能保证以前的全部指令执行完,它以后的指令必须从cache或内存获取。在这里咱们用dmb就足够了,dmb指令带有参数。用来表达该barrier生效的Shareability Domain(NSH表示Non-shareable、ISH表示Inner Shareable、OSH表示Outer Shareable、SY表示Full system,缺省是SY)和内存操做类型(LD表示读操做,ST表示写操做,缺省表示读写操做),好比DMB ISHST 表示对Inner Shareability Domain的读写操做生效。在peterson算法这里是能保证正常工做的,或者直接用dmb sy。blog
flag[0] = true; turn = 1; asm("dmb ishst");
//asm("dmb sy");
while (flag[1] && (turn == 1));
因为汇编指令和平台相关,移植不便。4.4.0和以后版本的gcc方便地提供了__sync_synchronize
函数完成内存栅栏指令。
flag[0] = true; turn = 1; __sync_synchronize(); while (flag[1] && (turn == 1));