什么是信号量?信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。
当它的值大于0时,表示当前可用资源的数量;p(可理解占用)
当它的值小于0时,其绝对值表示等待使用该资源的进程个数。v(可理解生产,释放占用资源)
注意,信号量的值仅能由PV操作来改变。
一般来说,信号量S³0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1;若S£0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。p操作(wait):申请一个单位资源,进程进入
经典伪代码
v操作(signal):释放一个单位资源,进程出来
三个同步问题
- 生产者-消费者问题
- 哲学家进餐问题
- 读者-写者问题
注:所有的都是,当缓冲池满了,生产者就不可往进存放,必须等待;当缓冲池空了,消费者就不可从中取出,必须等待
定义:
semaphore: 信号量
wait:(P操作)申请资源
signal:(V操作)释放资源
S:可用资源数目;当S<0, 表示有某些进程正在等待该资源,S的绝对值表示当前等待资源的进程数
mutex:互斥信号量(互斥锁)
empty: 表示缓冲池中空缓冲区数量
full:表示缓冲池中满缓冲区数量(这两个称为资源信号量)
临界区:每个进程中访问临界资源的那段代码叫临界区
进入区:临界区前,用于检查临界资源是否正在被访问的那段代码,可进入临界区则改标志为被访问;作用:申请资源
退出区:临界区后,用于将临界区正在被访问的标志恢复为未被访问的那段代码;作用:释放资源
假定生产者与消费者中有n个缓冲区(临界资源包含缓冲区):利用mutex实现进程对缓冲区的互斥作用,利用信号量empty和full表示表示缓冲区的空的和满的数量。假定生产者消费者等效,缓冲区未满,生产者才能把消息送入缓冲池;缓冲区未空,消费者才能从缓冲区中取出一个消息。 伪代码如下:
Var mutex, empty, full: semaphore := 1, n, 0; //设置各个初始值
buffer: array[0, ..., n-1] of item; //代表缓冲区,用来存放消息
in, out: integer := 0, 0; //存入,取出地址初始值(类似指针)
begin
parbegin //生产者
proceducer: begin
repeat
...
producer an item nextp; //生产者“生产”一个消息
...
wait(empty); //生产者申请一个空缓冲区(即申请访问临界资源)
wait(mutex); //进程进入临界区,加锁,使其他进程无法访问该临界资源
buffer(in) := nextp; //把消息存入缓冲区(访问临界资源)
in := (in + 1) mod n; //存入地址指向下一个缓冲区
signal(mutex); //退出临界区,开锁
signal(full); //释放一个满缓冲区
until false;
end
consumer: begin //消费者
repeat
wait(full); //消费者申请一个满缓冲区(即申请访问临界资源)
wait(mutex); //进入临界区,加锁
nextc := buffer(out); //取出缓冲区中消息(访问临界资源)
out := (out + 1) mod n; //取出地址指向下一个缓冲区
signal(mutex); //退出临界区, 开锁
signal(empty); //释放空缓冲区
consumer the item in nextc;
until false
end
parend
end
注: 每个程序中wait(mutex)和signal(mutex)必须成对存在,另外资源信号量也同理,且两个wait不能互相变换顺序; 当in=out时,缓冲区为空
利用Swait代替wait,用Ssignal代替signal
Var mutex, empty, full: semaphore := 1, n, 0;
buffer: array[0, ..., n-1] of item;
in, out: integer := 0, 0;
begin
parbegin
proceducer: begin
repeat
...
producer an item nextp;
...
//空缓冲区和锁都获取到才能访问(防止死锁)
Swait(empty, mutex);
buffer(in) := nextp;
in := (in + 1) mod n;
Ssignal(mutex, full);
until false;
end
consumer: begin
repeat
Swait(full, mutex);
nextc := buffer(out);
out := (out + 1) mod n;
Ssignal(mutex, empty);
consumer the item in nextc;
until false
end
parend
end
由于信号量机制中wait和signal分散在各个进程中进行(每个进程必须自己有PV操作),比较麻烦,容易死锁。so,用管程(同步工具)。
管程简单的讲就是一个门(感觉就是一个带同步功能的接口),他把共享数据和对数据的操作关在里面,而每次只能进一个进程(实现同步),外面的进程不能访问到管程内,而且进入管程的进程只能调用管程提供的对数据操作的功能。
PC:管程
count:缓冲池中已有的产品数目(count>=n时,表示缓冲池已满;count<=n时,表示缓冲池空了)
put(item):生产者把生产的产品投放到缓冲池
get(item):消费者从缓冲池中取出一个产品
producer: begin
repeat
produce an item int nextp;
PC.put(item);
until false;
end
consumer: begin
repeat
PC.get(item);
consumer the item in nextc;
until false;
end
桌上的筷子可视为临界资源,每个时间只允许一个哲学家使用。那么可用一个信号量表示一个筷子(这里有5个信号量),则
Var chopstick: array[0,...,4] of semaphore;
... //把所有信号量初始化为1
repeat //第i个哲学家活动如下
wait(chepstick[i]); //获取左边的筷子
wait(chepstick[(i + 1) mod 5]); //获取下一个(右边)的筷子
...
eat; //都成功,进餐
...
signal(chepstick[i]);
signal(chepstick[(i + 1) mod 5]);
...
think;
until false;
注:上述机制可能导致死锁(比如每个人都得到了左边的筷子,等着右边的筷子。。。)
每个哲学家先获得两个临界资源(一双筷子)才能进餐
repeat
think;
Sswait(chopstick[(i + 1) mod 5], chopstick[i]);
eat;
Ssignal(chopstick[(i + 1) mod 5], chopstick[i]);
until false;
特点:
wmutex :写互斥信号量
rmutex :读互斥信号量
readcount:正在读的进程数目(当Readcount=0时,才需要执行Wait(Wmutex))
Reader:读进程
Writer:写进程
Var rmutex, wmutex: semaphore :=1, 1;
Readcount: integer := 0; //读进程数目初始化0
begin
parbegin
Reader: begin //读操作
repeat
wait(rmutex); //申请读 进入临界区 加锁
//判断有没有读进程(目前为0则没有读进程,可以需要把写进程信号量改为“在写”,并不是真的在写,而是由于有进程申请了读,为了互斥,则要禁止写,则把写的信号量变为“在写”,使其他写进程不能进入临界区(去写))
if readcount=0 then wait(wmutex);
Readcount:= Readcount + 1; //读进程加1,表示多一个读进程在读
signal(rmutex); //释放锁 其他进程可以读
...
perform read operation; //读操作(上面是申请开始读,下面是申请读完)
...
wait(rmutex); //进入临界区 加锁 (读完了)
//判断读进程数目(为0,则要把写信号量释放,表示其他写进程可以申请写了)
if readcount=0 then signal(wmutex);
signal(rmutex); //释放锁 其他进程可以读
until false;
end
Writer: begin //写操作
repeat
wait(wmutex);
perform write operation
signal(wmutex);
until false;
end
parend
end
信号量集机制特点
so,该方法与记录型信号量相比,增加了一个限制:最多只允许RN个读者同时读
L :可申请使用的读信号量 初始化为RN
mx:写信号量
wait(L, 1, 1): 使L的值减1,当有RN个读者,则L减为0,再有wait操作就失败而阻塞
Var RN integer;
L, mx: semaphore := RN, 1;
begin
parbegin
Reader: begin
repeat
Swait(L, 1, 1); //查看读进程的数目是否小于设定值(这里是1),如果不小于则可以通过
Swait(mx, 1, 0); //申请当mx为1,则可以申请到(mx>0),则可以进行读操作,表示没有写操作在进行
...
perform read poreation;
...
Ssignal(L, 1); //释放读临界区
until false;
end
Writer: begin
repeat
Swait(mx, 1, 1; L, RN, 0);//既当无写进程(mx=1),又无读进程(L=RN)进行时,才可以写操作
perform write operation;
Ssignal(mx, 1);
unitl false;
end
parend
end
@(操作系统)[进程同步]
假设有一个理发店只有一个理发师,一张理发时坐的椅子,若干张普通椅子顾客供等候时坐。没有顾客时,理发师就坐在理发的椅子上睡觉。顾客一到,他不是叫醒理发师,就是离开。如果理发师没有睡觉,而在为别人理发,他就会坐下来等候。如果所有的椅子都坐满了人,最后来的顾客就会离开。
在出现竞争的情况下问题就来了,这和其它的排队问题是一样的。实际上,与哲学家就餐问题是一样的。如果没有适当的解决方案,就会导致进程之间的“饿肚子”和“死锁”。
如理发师在等一位顾客,顾客在等理发师,进而造成死锁。另外,有的顾客可能也不愿按顺序等候,会让一些在等待的顾客永远都不能理发。
最常见的解决方案就是使用三个信号量(Semaphore):一个给顾客信号量,一个理发师信号量(看他自己是不是闲着),第三个是互斥信号量(Mutual exclusion,缩写成mutex)。一位顾客来了,他想拿到互斥信号量,他就等着直到拿到为止。顾客拿到互斥信号量后,会去查看是否有空着的椅子(可能是等候的椅子,也可能是理发时坐的那张椅子)。
如果没有一张是空着的,他就走了。如果他找到了一张椅子,就会让空椅子的数量减少一张,这位顾客接下来就使用自己的信号量叫醒理发师。这样,互斥信号标就释放出来供其他顾客或理发师使用。如果理发师在忙,这位顾客就会等。理发师就会进入了一个永久的等候循环,等着被在等候的顾客唤醒。一旦他醒过来,他会给所有在等候的顾客发信号,让他们依次理发。
顾客信号量 = 0
理发师信号量 = 0
互斥信号量mutex = 1 // 椅子是理发师和顾客精进程都可以访问的临界区
int 空椅子数量 = N //所有的椅子数量
理发师(线程/进程)
While(true){ //持续不断地循环
P(顾客) //试图为一位顾客服务,如果没有他就睡觉(进程阻塞)
P(互斥信号量) //如果有顾客,这时他被叫醒(理发师进程被唤醒),要修改空椅子的数量
空椅子数量++ //一张椅子空了出来
V(理发师) //现在有一个醒着的理发师,理发师准备理发,多个顾客可以竞争理发师互斥量,但是只有一个顾客进程可以被唤醒并得到服务
V(互斥信号量) //释放椅子互斥量,使得进店的顾客可以访问椅子的数量以决定是否进店等待
/* 理发师在理发 */
}
顾客(线程/进程)
while(true)
{ //持续不断地循环
P(互斥信号量) //想坐到一张椅子上
if (空椅子数量 > 0)
{ //如果还有空着的椅子的话
空椅子数量-- //顾客坐到一张椅子上了
V(顾客) //通知理发师,有一位顾客来了
V(互斥信号量) //顾客已经坐在椅子上等待了,访问椅子结束,释放互斥量
P(理发师) //该这位顾客理发了,如果理发师还在忙,那么他就等着(顾客进程阻塞)
/* 竞争到了理发师则该顾客开始理发 */
}
else
{ //没有空着的椅子
V(互斥信号标) //不要忘记释放被锁定的椅子
/* 顾客没有理发就走了 */
}
}
#include
#include
#include
#include
#include
#include
using namespace std;
int chairs = 10;
int empty_chairs = chairs; // 空椅子数
int customers = 0;
int barbers = 1;
condition_variable cv_barbers; // 当有顾客到时需通知理发师
mutex chairs_mtx, cus_mtx, bar_mtx;
/**
* 理发师进程,阻塞理发师的请况
* 1. 没有顾客,则睡觉(阻塞)
* 2. 访问临界区受阻,此时临界区正在被顾客访问
* @param i [description]
*/
void barber(int i)
{
while (true)
{
unique_lock lck(bar_mtx);
cv_barbers.wait(lck, []
{
if (customers > 0)
{
cout << "有顾客,理发师被唤醒" << endl;
return true;
}
else
{
cout << "没有顾客,理发师睡觉" << endl;
return false;
}
});
unique_lock lck2(chairs_mtx);
customers--;
empty_chairs++;
/* cut hair*/
cout << "理发师给顾客理发" << endl;
lck2.unlock();
// 理发时不断有顾客进来
this_thread::sleep_for(std::chrono::microseconds(10));
}
}
/**
* 顾客进程,阻塞顾客进程的情况有两种
* 1. 访问临界区(检查是否有空闲的椅子)时发现理发师进程也在访问临界区,P(chairs_mtx)
* 没有多余的椅子并不是阻塞顾客进程,直至有空闲椅子,而是直接离开,即该顾客不排队理发
* @param i [description]
*/
void customer(int i)
{
unique_lock lck(chairs_mtx);
if (empty_chairs > 0)
{
empty_chairs--;
customers++;
cv_barbers.notify_one();
cout << "顾客 " << i << " 等待理发" << endl;
//this_thread::sleep_for(std::chrono::milliseconds(100));
lck.unlock();
}
else
{
/* 进程退出,不再理发了 */
cout << "顾客 " << i << " 没有位置,离开" << endl;
lck.unlock();
/* leave */
}
}
int main()
{
thread t1 = thread(barber, 1);
vector v;
for (size_t i = 0; i < 20; i++)
{
v.push_back(thread(customer, i + 1));
}
t1.join();
for (size_t i = 0; i < v.size(); i++)
{
v[i].join();
}
system("pause");
return 0;
}