原子操作:一个函数或动作由一个或多个指令的序列实现,对外是不可见的。也就是说,没有其他进程可以看到其中间状态或者中断此操作。要保证指令的序列要么作为一个组来执行,要么不执行,对系统没有可见的影响。原子性保证了并发进程的隔离。
临界区:是一段代码,这段代码中进程将访问共享资源,当另外一个进程已经在这段代码中运行时,这个进程就不能再这段代码中执行。
死锁:两个或两个以上的进程因其中的每个进程都在等待其他进程做完某些事情而不能继续执行,这种情形叫做死锁。
活锁:两个或两个以上的进程为了响应其他进程都在等待其他进程中的变化而持续改变自己的状态但不做有用的工作,这种情形叫做活锁。
互斥:当一个进程在临界区访问共享资源时,其他进程不能进入该临界区访问任何共享资源,这种资源称为互斥。
竞争条件:多个线程或者进程在读写一个共享数据时,结果依赖于它们执行的相对时间,这种情形称为竞争条件。
饥饿:是指一个可运行的进程尽管能继续执行,但被调度程序无期限忽视,而不能被调度执行的情形。
生产者-消费者问题(操作系统)原理与实现
常用并发机制
信号量:用于进程间传递信号的一个整数值。在信号量上只有三种操作可以进行:初始化、递减和增加,这三种都是原子操作。递减操作可以用于阻塞一个进程,增加操作可以用于解除阻塞一个进程。也称为计数信号量或一般信号量。
二元信号量:只取0值和1值的信号量
互斥量:类似于二元信号量。关键区别在于为其加锁(设定值为0)的进程和为其解锁(设定值为1)的进程必须为同一个进程
管程:一种编程语言结构,在一个抽象数据类型中封装了变量、访问过程和初始化代码。管程的变量只能由管程自己的访问过程,每次只能有一个进程在其中执行。访问过程即临界区。管程可以有一个等待进程队列。
事件标志:作为同步机制的一个内存字。应用程序代码可以为标志中的每个位关联不同的时间。通过测试相关的一个或多个位,线程可以等待一个事件或多个事件。在全部的所需位都被设定至少一个位设定OR之前,线程会阻塞。
消息:两个进程交互信息的一种方法,也可以用于同步。
自旋锁:一个互斥机制,进程在一个无条件循环中执行,等待锁变量的值变成可用。
=======================================================
与二元信号量相关的一个概念是互斥量。两者的关键区别在于互斥量加锁的进程和互斥量解锁的进程必须是同一个进程。相比之下,可能是某个进程对二元信号量进行加锁操作,而由另一个进程为其解锁。
最公平的策略是先进先出FIFO:被阻塞时间最久的进程最先从队列释放。采用这个策略定义的信号量为强信号量。
---保证不会产生饥饿
下面我们来看看生产者消费者的问题:
问题描述:
有一个或多个生产者生产某种类型的数据(记录、字符),并放置在缓冲区中;有一个消费者从缓冲区中取数据,每次去一项;系统保证避免对缓冲区的重复操作,也就是说,在任何时候只有一个主体(生产者或者消费者)可以访问缓冲区。问题是要确保这种情况,当缓存已满时,生产者不会继续向其中添加数据;当缓存为空时,消费者不会从中移走数据。
我们先来看看一个使用二元信号量解决无线缓冲区生产者消费者问题的不正确方法。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
int n;
binary_semaphore s = 1, delay = 0; void producer() { while( true) { produce(); semWaitsB(s); append(); n++; if(n == 1) semSignalB(delay); semSignalB(s); } } void consumer() { semWaitB(delay); while( true) { semWaitB(s); take(); n--; semSignalB(s); consume(); if(n == 0) SemWaitB(delay); } } void main() { n = 0; parbegin(producer, consumer); } |
我们假设缓冲区是无限的,并且是一个线性的元素数组。
这种方法十分直观。生产者可以在任何时候自由的往缓冲区中增加数据项。它添加数据前执行semWaitB(s),之后执行semWaitB(s),以阻止消费者或任何别的生产者在添加操作过程中访问缓冲区。同时,当生产者在临界区中时,将n的值增加1,如果n=1,则在本次添加之前缓冲区是空的。因此生产者执行semSignalB(delay),通知消费者这个事实。消费者在一开始时就使用semWaitB(delay),等待生产第一个项目,然后他在自己的临界区中取到这一项并将n减去1.如果生产者总能够保值在消费者之前工作,即n为正,则消费者很少会阻塞在delay上。
但是这种方法实际上是有问题的:当消费者消耗尽缓冲区中的项时,它需要重置信号量delay,因此等待生产者往缓冲区中放置了更多项。会导致消费者消费缓冲区中不存在的项。
解决这个问题的方法是引入一个辅助变量,可以在消费者的临界区中设置这个变量供以后使用。通过仔细的跟踪这个逻辑,可以确认不会发生死锁。
下面是使用三元信号量解决无线缓冲区生产者消费者问题的正确方法。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
int n;
binary_semaphore s = 1, delay = 0; void producer() { while( true) { produce(); semWaitsB(s); append(); n++; if(n == 1) semSignalB(delay); semSignalB(s); } } void consumer() { int m; semWaitB(delay); while( true) { semWaitB(s); take(); n--; m = n; semSignalB(s); consume(); if(m== 0) SemWaitB(delay); } } void main() { n = 0; parbegin(producer, consumer); } |
如果使用一般信号量(也称为计数信号量),可以得到更清晰的解决方法。变量n为信号量,它的值等于缓冲区的项数。假设在抄录这个程序时发生了错误,操作semSignal(s)和semSignal(n)被互换,这要求生产者在临界区中执行semSignal(n)操作不会被消费者或另一个生产者打断。
下面是使用信号量解决无限缓冲区生产者消费者问题的方法。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
binary_semaphore n = 0, delay = 1; void producer() { while( true) { produce(); semWaits(s); append(); semSignal(s); semSignal(n); } } void consumer() { while( true) { semWait(n); semWait(s); take(); n--; m = n; semSignal(s); consume(); } } void main() { n = 0; parbegin(producer, consumer); } |
最后,让我们给生产者消费者问题解决一个新的实际约束,即缓冲区是有限的。缓冲区是有限的。缓冲区被视为一个循环存储器,指针值必须表达为按缓冲区的大小取模,并保持下面的关系:
被阻塞 解除阻塞
生产者:往一个满的缓冲区中插入 消费者:移出一项
消费者:从空缓冲区中移出 生产者:插入一项
生产者和消费者函数可以表示如下(变量in和out初始化为0,n代表缓冲区的大小):
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
producer:
while( true) { //生产v while((in + 1) % n == out) //不做任何事 b[in] = v; in = (in + 1) % n; } cousumer: while( true) { while(in == out) //不做任何事 w = b[out]; out = (out + 1) % n; //消费者 } |
下面给出一种使用一般信号量的解决方案,其中增加了信号量e来记录空闲空间的数目。实现如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
const
int sizeofbuffer = 缓冲区大小;
binary_semaphore s = 1, n = 0, e = sizeofbuffer; void producer() { while( true) { produce(); semWait(e); semWait(s); append(); semSignal(s); semSignal(n); } } void consumer() { while( true) { semWait(n); semWait(s); take(); semSignal(s); semSignal(e); consume(); } } void main() { parbegin(producer, consumer); } |