第二章 进程的描述与控制——经典的进程同步问题

2.4.4 信号量集的应用

1. 利用信号量实现进程互斥

为使多个进程能互斥地访问某临界资源,只需为该资源设置一互斥信号量mutex,并设其初始值为1,然后将各进程访问该资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。

这样,每个欲访问该临界资源的进程在进入临界区之前,都要先对mutex执行wait操作,若该资源此刻未被访问,本次wait操作必然成功,进程便可进入自己的临界区,这时若再有其它进程也欲进入自己的临界区,对mutex执行wait操作必定失败,因而此时该进程阻塞,从而保证了该临界资源能被互斥地访问。

当访问临界资源的进程退出临界区后,又应对mutex执行signal操作,以便释放该临界资源。

利用信号量实现两个进程互斥的描述如下: 设mutex为互斥信号量,初值为1,取值范围为(-1,0,1)。等于1时,表示两个进程皆为进入需要互斥的临界区;等于0时,表示有一个进程进入临界区运行,另外一个必须等待,挂入阻塞队列;等于-1时,表示有一个进程正在临界区,另外一个进程因等待而阻塞在信号量队列中,需要被当前已在临界区运行的进程退出时唤醒。

注意:wait(mutex)和signal(mutex)必须成对地出现

2. 利用信号量实现前趋关系

设有两个并发执行的进程 P 1 P_{1} P1 P 2 P_{2} P2 P 1 P_{1} P1中有语句 S 1 S_{1} S1 P 2 P_{2} P2中有语句 S 2 S_{2} S2。我们希望在 S 1 S_{1} S1执行后再执行 S 2 S_{2} S2

我们只需使进程 P 1 P_{1} P1 P 2 P_{2} P2共享一个公用信号量S,并赋初值为0,将signal(S)操作放在语句 S 1 S_{1} S1后面,而在 S 2 S_{2} S2语句前面插入wait(S)操作,即
在进程 P 1 P_{1} P1中,用 S 1 S_{1} S1;signal(S);
在进程 P 2 P_{2} P2中,用wait(S); S 2 S_{2} S2

由于S被初始化为0,这样,若 P 2 P_{2} P2先执行必定阻塞,只有在进程 P 1 P_{1} P1执行完 S 1 S_{1} S1;signal(S);操作后使S增为1 P 2 P_{2} P2进程方能成功执行语句 S 2 S_{2} S2

2.4.5 管程机制

在信号量机制中,每个要访问临界资源的进程都必须具备同步操作wait(S)和signal(S)。这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。针对于此,产生了一种新的进程同步工具——管程(Monitors)。

1. 管程的定义

系统中的各种硬件资源均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略它们的内部结构和实现细节。

因此,可利用共享数据结构抽象地表示系统中的共享资源,并且将对该共享数据结构实施的特定操作定义为一组过程。进程对共享资源的申请、释放和其它操作必须通过这组过程,间接地对共享数据结构实现操作。

代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块,称之为管程。
由上述定义,可将管程分为四部分:

  1. 管程的名称
  2. 局部与管程的共享数据结构的说明
  3. 对该数据结构进行操作的一组过程
  4. 对局部于管程的共享数据结构设置初始值的语句、

实际上,管程中包含了面向对象的思想,将共享资源的数据结构及其操作包括同步机制封装在一个对象内部。封装与管程内部的数据结构仅能被封装与管程内部的过程所访问,任何管程外的过程都不能访问它;且封装与管程内部的过程也仅能访问管程内的数据结构

所有进程要访问临界资源时,都只能通过管程间接访问,而管程每次只准许一个进程进入管程,执行管理内的过程,从而实现进程互斥。

管程的特性:

  1. 模块化
  2. 抽象数据类型
  3. 信息掩蔽

管程与进程的区别:

  1. 进程定义的是私有数据结构PCB,管程是公共数据结构。
  2. 进程由顺序程序执行对数据结构上的操作,而管程主要是进行同步操作和初始化操作
  3. 进程目的是实现系统的并发性,管程是解决共享资源的互斥问题
  4. 进程为主动工作方式,管程为被动
  5. 进程具有动态性,而管程是操作系统的一个资源管理模块,供进程调用。

2. 条件变量

在利用管程实现进程同步时,必须设置同步工具。如果仅有wait和signal是不够的。考虑一种情况:当一个进程调用管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,在此期间,如果进程不释放该管程,则其它进程无法进入管程,被迫长时间等待。

为解决这个问题,引入了条件变量condition(只能在管程内部进程)。管程中对每个条件变量都需予以说明,其形式为:condition x,y; 对条件变量的操作仅仅是wait和signal,因此条件变量也是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时,提供了两个操作即可表示为x.wait和x.signal,其含义为:

x.wait:正在调用管程的进程因条件x需要被阻塞或挂起,则调用x.wait将自己插入到x条件的等待队列上,并释放管程,直到x变化。此时其它进程可以使用该管程。

x.signal:正在调用管程的进程发现x变化,则调用x.signal,重新启动一个因x条件而阻塞或挂起的进程。如果存在多个这样的进程,则选择其中一个,如果没有,则继续执行,而不产生任何结果。

2.5 经典进程的同步问题

2.5.1 生产者-消费者问题

1. 利用记录型信号量解决生产者-消费者问题

假定在生产者和消费者之间的公用缓冲池中具有n个缓冲区,这时可利用互斥信号量mutex实现诸进程对缓冲池的互斥使用;利用信号量empty和full表示缓冲池中空缓冲池和满缓冲池的数量。又假定生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息,其描述如下:

int in=0,out=0;
item buffer[n];
semaphore mutex=1,empty=n,full=0;
void producer()
{
	do{
		producer an item nextp;
		..
		wait(empty);
		wait(mutex);
		buffer[in]=nextp;
		in=(in+1)%n;
		signal(mutex);
		signal(full);
	}while(TRUE);
}
void consumer()
{
	do{
		wait(full);
		wait(mutex);
		nextc=buffer[out];
		out=(out+1)%n;
		signal(mutex);
		signal(empty);
		consumer the item in nextc;
		...
	}while(TRUE);
}
void main()
{
	cobegin
		producer();
		consumer();
	coend;
}

注意:对资源信号量的操作都必须成对出现

2. 利用AND信号量解决生产者-消费者问题

在这个信号量机制中,只需将上面的wait、signal操作的分别合并即可,例如用Swait(empty,mutex)来代替wait(empty)和wait(mutex)等。

3. 利用管程解决生产者-消费者问题

用此方法首先应为它们建立一个管程,并命名为producerconsumer,或简称PC。其中包括两个过程:

put(x):生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变量count来表示在缓冲池中已有的产品数目,当count ≥ \geq N时,表示缓冲池已满,生产者必须等待。

get(x):消费者利用该过程从缓冲池中取出一个产品,当count ≤ \leq 0时,表示缓冲池中已无可取用的产品,消费者应等待。

对于条件变量notfull和notempty,分别由两个过程cwait和csignal对它们进程操作。

cwait(condition):当管程被一个进程占用时,其它进程调用该过程时阻塞,并挂在条件condition的队列上。

csignal(condition):唤醒在cwait执行后阻塞在条件condition队列上的进程,如果这样的进程不止一个,则选择其中一个实施唤醒操作;如果队列为空,则无操作而返回。
PC管程可描述如下:

Monitor producerconsumer
{
	item buffer[N];
	int in,out;
	condition notfull,notempty;
	int count;
	public;
	void put(item x)
	{
		if(count>=N)
			cwait(notfull);
		buffer[in]=x;
		in=(in+1)%N;
		count++;
		csignal(notempty);
	}
	void get(item x)
	{
		if(count<=0)
			cwait(notempty);
		x=buffer[out];
		out=(out+1)%N;
		count--;
		csignal(notfull);
	}
	{ in=0;out=0;count=0;}
}PC;	

生产者-消费者问题可描述如下:

void producer()
{
	item x;
	while(TRUE)
	{
		...
		produce an item in nextp;
		PC.put(x);
	}
}
void consumer()
{
	item x;
	while(TURE)
	{
		PC.get(x);
		consume the item in nextc;
		...
	}
}
void main()
{
	cobegin
	producer();
	consumer();
	coend
}

2.5.2 哲学家进餐问题

该问题是描述有五个哲学家公用一张圆桌,分别坐在周围的五张椅子上,再圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。

1. 利用记录型信号量解决哲学家进餐问题

由已知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现筷子的互斥使用, 可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组,其描述如下:

semaphore chopstick[5]={1,1,1,1,1};
do{
	wait(chopstick[i];
	wait(chopstick[(i+1)%5]);
	...
	//eat
	...
	signal(chopstick[i]);
	signal(chopstick[(i+1)%5]);
	...
	//think
	...
}while(TRUE);

虽然上述解法可保证不会有两个相邻的哲学家同时进餐,但却有可能引起死锁。假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量chopstick均位0;当他们再试图去拿右边的筷子时,都将因无筷子可拿而无期限地等待。对于这种死锁问题,可采用如下几种解决办法:

  1. 至多允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子。
  2. 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
  3. 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。

2. 利用AND信号量机制解决哲学家进餐问题

其描述如下:

semaphore chopstick[5]={1,1,1,1,1};
do{
	...
	//think
	...
	Swait(chopstick[(i+1)%5],chopstick[i]);
	...
	//eat
	...
	Signal(chopstick[(i+1)%5],chopstick[i]);
}while(TRUE);


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
读者与写者问题是多进程并发编程中的经典问题,主要解决的是多个进程同时对共享数据进行读写操作时可能产生的数据不一致性问题。具体来说,该问题假设有若干个读者和一个写者进程,它们同时访问某个共享资源(如一个文件、一个数据库等),读者只读取共享资源而不修改它,而写者则修改共享资源。 由于读者和写者对共享资源的访问方式不同,因此需要对它们的访问进行调度和同步,以防止读写冲突导致数据不一致。下面是一种实现读者与写者问题的方法: ```python from threading import Lock class ReaderWriter: def __init__(self): self.read_count = 0 self.mutex = Lock() # 互斥锁,用于保护共享资源的访问 self.write_lock = Lock() # 写锁,用于保证写操作的原子性 def start_read(self): self.mutex.acquire() self.read_count += 1 if self.read_count == 1: self.write_lock.acquire() # 第一个读者获取写锁,防止写者进程修改共享资源 self.mutex.release() def end_read(self): self.mutex.acquire() self.read_count -= 1 if self.read_count == 0: self.write_lock.release() # 最后一个读者释放写锁,允许写者进程修改共享资源 self.mutex.release() def start_write(self): self.write_lock.acquire() # 写锁保证写操作的原子性 def end_write(self): self.write_lock.release() # 释放写锁 ``` 在上述代码中,`ReaderWriter` 类实现了读者与写者的同步和调度。具体来说,它定义了三个方法: - `start_read`:读者开始读取共享资源时调用该方法,它首先获取互斥锁,然后增加读者计数器,如果是第一个读者,则获取写锁,以防止写者进程修改共享资源。 - `end_read`:读者读取完共享资源时调用该方法,它首先获取互斥锁,然后减少读者计数器,如果是最后一个读者,则释放写锁,允许写者进程修改共享资源。 - `start_write`:写者开始修改共享资源时调用该方法,它直接获取写锁,保证写操作的原子性。 - `end_write`:写者修改完共享资源时调用该方法,它释放写锁。 在实际使用中,可以将共享资源作为 `ReaderWriter` 类的一个属性,并在读者和写者进程中调用相应的方法,以实现进程同步和数据一致性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值