同步与互斥问题分析
0x00 并发的两个问题
同步与互斥是并发的两个问题,本质是执行流间的约束关系。互斥是针对资源的访问而言,不能同时访问,是一种间接约束关系,表明不能同时执行某些代码。同步指多个执行流要按照一定顺序,是一种直接约束关系。
0x01 实现互斥
互斥有几个原则,空闲让进,忙则等待,有限等待,让权等待,说人话就是,能用就用,目前不能用就先不用,但是最终一定要能用到,我不能用的时候我去歇着让别人来(执行)。 锁解决了前三条,谁用谁加锁,加锁后只能自己用,用完了把锁打开。因此,互斥的访问一个资源,就是尝试获得锁的过程。怎么获得锁呢?又有两种解决方案:自旋锁和互斥锁。前者类似while循环,循环里就一件事不断试图拿到锁,拿到就出循环,去访问资源,然后释放。后者则是试图获得锁,拿不到就阻塞,直到有人释放锁,再来试图拿,以此类推,第四条也解决了。 自旋锁的好处是,减少了系统调用,坏处是可能长时间空loop;互斥锁则相反。
注意点:锁只是实现了互斥,锁之间的代码是“相对”于其他访问同种临界的执行流具有原子性,而相对于没有访问此种临界资源的执行流没有原子性,因此加锁解锁之间仍会有执行流的转移,有造成死锁的可能
0x02 实现同步
以下是几种工具,解决不同场景的问题: ①信号量,实现(可多个同时访问的)的互斥或者(特定的)前驱后继关系。主要原理,信号量可以设置初始,两个操作,wait和signal 又称p,v。p操作执行原子操作{ 信号量–,判断减后是否小于0,如果小于0则加入阻塞队列 } v操作执行原子操作{信号量++,判断+后是否<=0,则从对应阻塞队列拿一个出来放在就绪队列并将锁给该执行流}。 用信号量解决前驱后继关系有个特点,就是一次p满足一个v,也就是如果想要让bcd三个执行流在a后执行,需要a调用三个v操作,而bcd各自调用一次p,更适合用条件变量。 ②条件变量,实现前驱后继关系,需要和互斥锁一起用。主要操作 wait signal。前者相对其他访问条件变量的执行流来说原子地释放锁和阻塞,后者唤醒阻塞在该条件变量的线程并给它锁。 为什么需要使用锁?两个原因:①条件是临界资源,访问需要互斥②条件变量(这里主要指其中的保存的阻塞队列)也是临界资源,访问也需要互斥。根据man里signal执行流的实例,推测出条件变量本身并不保证对自身队列的互斥访问,需要和互斥锁配合。
//wait使用过程
lock(&mutex);
while(not_fit(condition) ){ //为什么用while 因为根据man的说明signal可能依次唤醒多个wait,按照唤醒的次序给锁,因此对于后唤醒的而言,条件可能已经不成立了(前面的执行流可能修改了)。
wait(&condition,&mutex);//释放锁并阻塞 原子操作(同样也是相对的原子)
}
unlock(&mutex);
//signal使用过程
lock(&mutex);
enable(&condition);
signal(&condition,&mutex);
unlock(&mutex);//unlock一定要在signal后面,是上面的原因2,对条件变量的阻塞队列也要互斥
为什么要用这么复杂的条件变量?首先考虑能不能直接用互斥锁解决前驱后继关系,当然可以,需要条件的地方,直接while(true){ 加锁;判断是否满足条件,满足就释放锁,并break;释放锁 } 。满足条件的地方就 {加锁;修改条件;释放锁}。这种方法有个问题就是效率很低,不断执行加锁释放锁。
0x03 用条件变量实现信号量
我们知道信号量的伪代码是:
//P操作
semaphore--;
if(semaphore<0) block in semaphore_list
//v操作
semaphore++;
if(semaphore<=0) move an node to read_list from semaphore_list
上面两个操作都可以用条件变量实现
//p操作
lock(&mutex);
cond.value--;
while(cond.value<0) wait(&cond,&mutex);
unlock(&mutex);
//v操作
lock(&mutex);
cond.value++;
if(cond<=0) signal(&cond);
unlock(&mutex);