上篇文章《
线程同步---睡觉与唤醒
》遗留了一个问题,就是会出现死锁的情况。所以为了解决这个问题,又要引入一个新的同步概念,那就是信号量。
起初我个人在理解的时候陷入了一个奇怪的圈子,就总凭感觉和名字认为这个信号量是不是一个储存我之前失效信号的容器,让后在需要之前信号的时候在重新调用信号。
其实没有那么复杂。书上一堆绕来绕去的话令刚接触的人的确不太好想,总之先忘了上篇讲到的sleep和wake吧!
以我的理解来概括信号量的话,就是一把可以让对方睡眠或唤醒的智能锁。
我们都知道之前接触的锁都是一个线程进去,把锁上锁了,其他进程就只能在锁外面等着它执行完,释放了锁才能进入刚被锁住的区域。而信号量就好比把之前的锁优化了,其他进程被锁堵在外面会去睡觉,而不是等待!等占有锁的线程把临界区的代码执行完了,会去唤醒刚去睡觉的其他线程。
而信号量中这个 量 字不难理解,肯定和数量有关,其实就是个计数器。
想想以前的锁,是不是只有两种情况,要么开着,要么锁着。要么计数器是0没有锁(锁被人拿走了,代表已经有某个线程进入临界区了),要么计数器是1有锁(没人拿走锁,代表临界区现在没有线程了。我可以把这个1拿走让这个计数器变为0,我进入临界区,后面来的线程看见计数器是0了,就明白是什么意思睡觉去了。)
那么结合上篇x取0-10的这个例子,我们是不是能让这个计数器的范围扩大一下。
上篇文章的例子:
A线程负责给x++,只要x不到10的时候就必须给x加1 。如果x为10了,自己就去睡觉。.
B线程负责给x--,只要x不为0,B就一直让x-1。如果x为0,自己就去睡觉
。
对于A线程来说,只要计数器不为10,A都可以获得一把锁进入临界区。
对于B线程来说,只要计数器不为0,B也都可以获得一把锁进入临界区。
而表示计数器加减的用up()和down()
down表示只要计数器是不为0的,即≥1的情况,就可以拿一把锁,让计数器减1,
up表示计数器不超过计数器上限的。即≤MAX-1的情况,就可以归还一把锁,让计数器加1.
那么具体实现套到具体例子来看看。
定义 int x=2; //随便给x一个初始值
int MAX=10;
int a1=2; //x的个数
int a2=8; //x还差多少才能满的个数
int tmp=1; // 相当于一把1和0的互斥锁
A:
down( a2); //如果x距满的个数还差 不为0个(意思就是不是满的),就可以进入临界区对x++。
down(tmp);
x++;
up(tmp);
up(a1); //由于x++了。所以释放锁的时候应该让 表示x个数的a1去++
B:
down(a1); //如果x的个数不为0,就可以进入下面临界区,对x进行--
down(tmp);
x--;
up(tmp);
up(a2); .//由于x--了,所以释放锁的时候应该让 表示差多少个才能满的a2++
至于上面为什么A和B的两个down下面又加了一对down(tmp)/up(tmp)呢?
就是防止一种情况。A和B在大部分情况都是可以同时进入临界区的,比如就那x=4来说,x是4,a1=4不是0;x距离满还差6个,a2=6也不是0,A和B都可以进入下面的临界区对x进行修改,那不就又出现的无法保护临界区数据的情况。所以要在修改x的前后加入一把锁,这把锁就只有0,1两个选项,也就是说哪怕A,B同时进入临界区了也无所谓,反正A,B俩个线程之中只有一个会得到tmp这把锁。才能对x修改。
那会不会出现死锁的情况呢,AB线程,无论谁先完成一步up了,cpu再怎么切换。都会对a1或a2进行一次up。那就代表a1和a2至少有一个不是0,死
锁发生的条件是cpu的切换导致唤醒函数失去作用让两个线程同时睡着了没人叫。刚说了a1和a2至少有一个不为0,是不是代表至少有一个不会被down(0)判断成功使它睡觉去。既然同时睡觉成了不可能的事情,那么死锁就不会发生了。(当然最起码的条件是down和up的位置相互匹配,如果down(a1)和down(tmp)的位置交换,逻辑都不对了,死锁产生是必然的)。