系统中可能有着许多并发的进程,在这些进程之间存在这些关系:
(1)间接制约关系:多个进程之间共享临界资源,例如打印机,全局变量。要保证各个进程互斥的访问临界资源,这种关系叫做进程互斥。
(2)直接制约关系:进程之间存在着某些时序关系,例如进程A需要在进程B执行完后才能执行,这种关系就是进程同步。(linux的内核同步参考:http://blog.csdn.net/lsjseu/article/details/10946119)
详细总结请看:经典线程同步总结
临界区
每个进程中,访问临界资源的那段代码叫做临界区。要保证各进程互斥进入临界区。在临界区前面设立一个段进入区,后面加上一段退出区。
while(true){
entry section;//进入区
critical section;
exit section;//退出区
}
为了实现互斥访问,有如下准则:
(1)空闲让进
(2)忙则等待
(3)有限等待:保证进程在有限时间进入临界区,不要把lz”饿死“。
(4)让权等待:等待的时候,让出CPU,不要让CPU空跑。(注意:linux的自旋锁就让CPU空跑了,但是开销很小)
信号量机制
信号量是一个记录性数据。信号量上提供了两种原子型的操作。即P操作,表示申请资源;V操作,表示释放资源。定义如下:
struct sem{
int value;
pointer_PCB queue;
}
void p(s)
{
s.value = s.value - 1;
if(s.value<0)//小于0,表示当前无资源,自己阻塞到等待队列
block(s.queue);
}
void v(s)
{
s.value = s.value + 1;
if(s.value<=0)//小于等于0,表示当前没有资源,但是我释放了一个资源,可以唤醒等待队列的第一个进程;
wakeup(s.queue);//如果大于0,表示资源有多余的,进程申请资源肯定能申请,无需唤醒任何进程
}
注:信号量用于互斥的时候,取值只能为1或者0,;但是为了实现同步的时候,就不一定小于1了,具体问题具体分析。多个生产者和消费者我就搞错了。信号量实现资源互斥访问
利用P/V操作很容易实现,资源互斥访问。因此,可以设置一个互斥信号量mutex=1(只能是0或者1),每个临界区插入p、v操作代码:
void process()
{
while(true){
p(mutex);
critical section;
v(mutex);
}
}
信号量实现进程同步
信号量主要用于进程之间的同步问题。这里总结一个规律:P1进程一定要在P2进程后面执行,则定义一个信号量,在P1实现P操作(后继节点实现P操作,相对于申请资源),P2中实现V操作(前驱节点实现V操作,相对于释放资源)。这也说明进程同步的P、V操作分布在不同的进程中。
在windows编程里面实现线程同步可用关键段和互斥量,详细参考:
《秒杀多线程第五篇经典线程同步关键段CS》
《秒杀多线程第七篇经典线程同步互斥量Mutex》
例1:p1中的s1一定要在p2的s2执行过之后,根据上面分析,这里之存在一层同步关系,定义一个信号量sync。
sem sync = 0;
void p1()
{
p(sync)
s1;
}
void p2()
{
s2;
v(sync);
}
注:在windows编程中,不要用关键段来实现sync信号量,因为关键段不能跨进程使用。最好用事件(Event)或者信号量。事件,互斥量,信号量都是内核对象,可以跨进程使用。
例2;如下图的前驱关系,实现同步
sem f1,f2,f3,f4,f5={0,0,0,0,0};
void s1(){
....
v(f1);//两个后继
v(f1);
}
void s2(){
p(f1);//前驱s1
....
v(f2);//两个后继
v(f2);
}
void s3(){
p(f1);//前驱s1
....
v(f3);//一个后继
}
void s4(){
p(f2);//前驱s2
....
v(f4);//一个后继
}
void s5(){
p(f2);//前驱s2
....
v(f5);//一个后继
}
void s6(){
p(f3);//前驱s3
p(f4);//前驱s4
p(f5);//前驱s5
}
注:有前驱和后继关系就要申请信号量,实现进程同步。
生产者-消费者问题
windows编程线程同步请参考:
《秒杀多线程第六篇经典线程同步事件Event》
《秒杀多线程第八篇经典线程同步信号量Semaphore》
(1)一个生产者、一个消费者和一个缓冲区
这里面有一个同步和互斥问题。同步:消费者要等到缓冲区满后才能消费,即生产者是消费者前驱。而生产者要等消费者要等缓冲区空后才能生产,即消费者是生产者前驱。互斥:消费者和生产者不能同时访问缓冲区。
生产者和消费者问题windows编程,参考:生产者-消费者问题
按照上面的分析分别设定如下信号量:
sem empty = 1;//生产者和消费者同步
sem full = 0;//生产者和消费这同步
sem mutex = 1;//缓冲区互斥
producer()
{
while(true){
p(empty);//消费者通知生产者
p(mutex);//保证buffer互斥访问
生产者生产放进buffer;
v(mutex);
v(full);//通知消费者可以消费
}
}
consumer()
{
while(true){
p(full);//由生产者通知
p(mutex);//保证buffer互斥访问
从buffer里面消费;
v(mutex);
v(empty);//通知生产者可生产
}
}
(2)多个生产者、多个消费者和多个缓冲区
一样的思路,多了两个变量in和out分别表示放进那个buffer缓冲区和从哪个缓冲区取。下面程序原本我忽略了一个问题,我是将empty=1,这就不对了。应该是这样的sem empty = MAX(MAX表示你的缓冲区个数)。如果还是初始化为1,每次只能限制生产一个产品,而且还有等待消费者把这个产品消费掉后,生产者才会去生产,而且最终谁去生产,就看谁先抢到empty这个信号量了。在windows编程里面可以用下语句:
Empty = CreateSemaphore(NULL, 4, 4, NULL); //前一个值表示,初识值为4,而且最大值可以为4。
Full = CreateSemaphore(NULL, 0, 4, NULL); //初识为0,最大值可以达到0。
之前看这个程序一直纳闷,现在彻底明白了,彻底懂了,更正如下:
const int MAX = 4;//缓冲区的个数
sem empty = MAX ;//生产者和消费者同步
sem full = 0;//生产者和消费这同步
sem mutex = 1;//缓冲区互斥
int in = out = 0;
producer()
{
while(true){
p(empty);//消费者通知生产者
p(mutex);//保证buffer互斥访问
in = (in + 1)%MAX;
生产者生产放进buffer[in];
v(mutex);
v(full);//通知消费者可以消费
}
}
consumer()
{
while(true){
p(full);//由生产者通知
p(mutex);//保证buffer互斥访问
out = (out + 1)%MAX;
从buffer[out]里面消费;
v(mutex);
v(empty);//通知生产者可生产
}
}
下面考虑这样一个问题:
const int MAX = 4;//缓冲区的数量
sem empty = 1;//生产者和消费者同步
sem full = 0;//生产者和消费这同步
sem mutex = 1;//缓冲区互斥
int in = out = 0;
producer()
{
while(true){
p(mutex);//交换
p(empty);//1
in =(in + 1)%MAX;
生产者生产放进buffer[in];
v(mutex);
v(full);
}
}
consumer()
{
while(true){
p(mutex);//2
p(full);//
out = (out + 1)%MAX;
从buffer[out]里面消费;
v(mutex);
v(empty);
}
}
说明:上面这段程序交换了p(mutex)和p(empty),带来什么后果呢?我们假设生产者生产跑得比较快,当生产者生产了一个商品后,消费者还没启动。当生产者进入语句//1,则由于buffer里面的东西没有被消费者干掉而等在这里。而此时,由于生产者进入临界区了,消费只能在外面等,无法进入临界区消费,只能在//2处等。这出现死锁现象。相当于生产者自己把门关了,却等着消费者来买东西,进不来啊,人家。
哲学家进餐问题
五个哲学家,坐在一个桌子上,五支筷子,五个碗。则有下面程序:
sem chopstick[5]={1,1,1,1,1}
void philosopher(int i)
{
while(true){
p(chopstick[i]);//取左边的筷子
p(chopstick[i+1]);//取右边的筷子
eating;//吃饭
v(chopstick[i]);
v(chopstick[i+1]);
thinking;//思考
}
}
会出现这么一瞬间:当所有的哲学家都拿到了左边的筷子,想去拿右边的筷子时候,跪了,被人占了。于是大家就只能死等了。
解决方法:
(1)只允许四个哲学家坐在桌边;
(2)当且仅当哲学家可以拿起两边的筷子时,才允许他们拿,这看谁抢得快,抢不到就等吧。
(3)奇数号的哲学家必须首先抓起左边的筷子,偶数相反。
读者与写者问题
临界区允许多个读者同时进入,而写者只能互斥进入。这类问题叫做“读者-写者问题”。两种处理方案,一种是读者优先,另一种是写者优先。它们之间的区别在于,当写者提出请求后,是否允许读者进入。如果允许进入,表示读者优先,否则写者优先。
读者-写者问题windows编程,参考:读者-写者问题
(1)错误的方法
这种实现方法只能保证每次进去一个人,要么读者要么写者。
//这种实现方案
sem mutex = 1;
p(mutex);//只能保证每次只能进入一个读者读(写)文件
读者或写者可进入临界区
v(mutex);
注:下面的程序当写者有多个的时候,不要简单的将信号量赋成1,不要犯同样的错误。
(2)读者优先方法
就是不管有没有写者,读者都可以进去读。感觉有点乱,下面程序是读者还是要等待写者写完了以后才能进去。
//下面实现一种读者优先的方案:
int readerCnt = 0;//记录当前的多少读者
sem hasreader = 0;//读者与写者之间的同步
sem mutex = 1;//reader变量互斥访问
Reader();//读者可以任意进入,读者来了,写者滚蛋
{
while(true){
p(mutex)
readerCnt++;//来一个读者,加一个读者
if(readerCnt>0)
p(hasReader);//如果此时,里面有写者,则阻塞读者,无写者读者可进入
v(mutex);
读者开始读书;
p(mutex)
readerCnt--;//读者读完离开
if(readerCnt==0)
v(hasReader);//没有读者的时候,才唤醒读者
v(mutex);
}
}
Writer()
{
while(true){
P(hasReader);
写者写书中...
V(hasReader);
}
}
上面的程序:如果一个写者已进入临界区且有n个读者正在等待,则只有一个读者在等待hasReader这个信号量排队。其他n-1个读者都在mutex上排队;另外,一个写者V(hasReader)以后,既可以允许一个正在等待的写者进入,也可以允许若干个正在等待的读者进入。
(3)写者优先方法
看了一个大牛写的写者优先思想:如果当前有写者在等待,则一定是写着先进入。读者进入的时候,要进行如下判断:一是否有写者正在写,这种情况肯定是不能进去的,只能在等待队列里面等;二是要判断写者等待的数量是否大于0,如果当前有写者在等待,还得继续等,因为写者优先嘛。
总结起来读者进入的条件:一是当前进入临界区一定是读者而且此时没有写者等等进入临界区,有写者正在写肯定是不能进入;二是有写者如果在等临界资源,只能给他让道;三是如果读者正在读的时候,来了一批写者,那么此时后面进来的读者也不能进入了,只能等当前正在读的读者进行完,然后这批写者进入写完,再才轮到这些读者,可怜啊。
注:读者优先反过来。
(4)读者和写者一样优先权
但是能保证读者多次进入,就是说,当前里面是读者,读者可以继续进入,但是写者不可以进入。写者只能互斥的进入。这里面存在两种前驱关系:
前驱关系1:没有写者,读者才能进入;
前驱关系2:没有读者,写者才能进入:
综上,应该是两个信号量,而不是一个信号量。以下代码:
sem mutex = 1;
p(mutex);//只能保证每次只能进入一个读者读(写)文件
v(mutex);
//下面实现一种读者优先权一样的方案:
int readerCnt = 0;//记录当前的多少读者
sem hasReader = 0;//读者,先让读者进入
sem hasWriter = 1;//写者
sem mutex = 1;//reader变量互斥访问
Reader();
{
while(true){
p(hasWriter)
p(mutex)
readerCnt++;//来一个读者,加一个读者
if(readerCnt>0)
p(hasReader);
v(mutex);
读者开始读书;
p(mutex)
readerCnt--;//读者读完离开
if(readerCnt==0)
v(hasReader);//唤醒读者
v(mutex);
}
}
Writer()
{
while(true){
P(hasReader);
写者写书中...
V(hasWriter);
}
}
利用读写锁解决读写者问题参考:读写锁