互斥量:和二元信号量很类似,但和信号量不同的是:信号量在一个系统中,可以被任意线程获取或释放。互斥量要求那个线程获取互斥量,那么哪个线程就释放互斥量,其他线程释放无效。
临界区:比互斥量更加严格的手段。把临界区的锁获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量,信号量区别在与互斥量,信号量在系统中任意进程都是可见的。临界区的作用范围仅限于本线程,其他线程无法获取。其他性质与互斥量相同。
读写锁:致力于一种更加特定的场合的同步。如果使用之前使用的信号量、互斥量或临界区中的任何一种进行同步,对于读取频繁,而仅仅是偶尔写入的情况会显得非常低效。读写锁可以避免这个问题。对于同一个锁,读写锁有两种获取方式:
读写锁的总结
读写锁状态以共享方式获取以独占方式获取
自由 成功 成功
共享 成功 等待
独占 等待 等待
条件变量:作为同步的手段,作用类似于一个栅栏。对于条件变量,线程有两个操作:
使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时,所有线程可以一起恢复执行。
可重入与线程安全一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。
一个函数要被重入,只有两种情况:
一个函数被称为可重入,表示重入之后不会产生任何不良影响
可重入函数:
1 int sqr(int x) 2 { 3 return x*x; 4 }
一个函数要成为可重入,必须具有如下特点:
可重入是并发安全的强力保障,一个可重入的函数可以在多程序环境下方向使用
过度优化有时候合理的合理的使用了锁也不一定能保证线程的安全。
//Thread1 x=0; lock(); x++; unlock(); //Thread2 x=0; lock(); x++; unlock();
上面X的值应该为2,但如果编译器为了提高X的访问速度,把X放到了某个寄存器里面,不同线程的寄存器是各自独立的,因此,如果Thread1先获得锁,则程序的执行可能会呈现如下:
[Thread1]读取x的值到某个寄存器R [1] (R[1]=0);
[Thread1]R[1]++(由于之后可能要访问到x,所以Thread1暂时不将R[1]写回x);
[Thread2]读取x的值到某个寄存器R[2] (R[2]=0);
[Thread2]R[2]++(R[2]=1);
[Thread2]将R[2]写回至x(x=1);
[Thread1] (很久以后)将R[1]写回至x(x=1);
如果这样,即使加锁也不能保证线程安全
x=y=0; //Thread1 x=1; r1=y; //Thread2 y=1; r2=x;
上面代码有可能发生r1=r2=0的情况。
CPU动态调度:在执行程序的时候,为了提高效率有可能交换指令的顺序。
编译器在进行优化的时候,也可能为了效率交换两个毫不相干的相邻指令的执行顺序。
上面代码执行顺序可能是这样:
x=y=0; [Thread1] r1=y; x=1; [Thread2] y=1; r2=x;
使用volatile关键字可以阻止过度优化,colatile可以做两件事情:
但volatile无法阻止CPU动态调度换序
C++中,单例模式。
volatile T* pInst=0; T* GetInstance() { if(pInst==NULL) { LOCK(); if(pInst==NULL) pInst=new T; unlock(); } return pInst; }
CPU的乱序执行可能会对上面代码照成影响
C++里的new包含两个步骤:
所以pInst=new T包含三个步骤:
这三步中2和3的步骤可以颠倒,可能出现这种情况:pInst中的值不是NULL,但对象还是没有构造完成。
要阻止CPU换序,可以调用一条指令,这条指令常常被称为barrier:它会阻止CPU将该指令之前的指令交换到barrier之后。
许多体系的CPU都提供了barrier指令,不过,它们的名称各不相同。例如POWERPC提供的指令就叫做lwsync。所以我们可以这样保证线程安全:
#define barrier() __asm__ volatile ("lwsync") volatile T* pInst=0; T* GetInstance() { if(pInst==NULL) { LOCK(); if(pInst==NULL) { T* temp=new T; barrier(); pInst=temp; } unlock(); } return pInst; }
1.6.3多线程的内部情况