操作系统——并发:互斥和同步

一、基本概念

  • 原子操作:一个函数(原语)或动作的指令序列不可分割,要么作为一个整体执行(不可中断),要么都不执行。
  • 临界资源:一次仅允许一个进程独自占有使用的不可剥夺资源。
  • 临界区:进程访问临界资源的那一段代码。
  • 互斥:当一个进程正在临界区中访问临界资源时,其他进程不能进入临界区。
  • 同步:合作的并发进程需要按先后次序执行。例如:一个进程的执行依赖于合作进程的消息或信号。当一个进程没有得到来自合作进程的消息或信号时需阻塞等待,直到消息或信号到达才唤醒。
  • 死锁:多个进程全部阻塞,形成等待资源的循环链
  • 饥饿:一个就绪进程被调度程序长期忽视、不被调度执行。
  • 信号量:用于进程间传递信号的一个整数。在信号两上只可以进行三种操作,即初始化,递增,递减。这三种操作分别都是原子操作。递减作用于阻塞一个进程,递增作用于解除一个进程的阻塞。信号量也称为计数信号量或一般信号量。

二、互斥

1、互斥的要求

  • 强制互斥(忙则等待):一次只允许一个进程进入临界区。有进程正在临界区执行时,其他请求进程等待;等待进程退出后,从多个请求进程中选择一个进入临界区。
  • 有限等待:请求进程应在有限的等待时间内进入临界区,不能造成进程死锁或饥饿。
  • 有空让进:当临界区空闲时,请求进程可立即进入。
  • 让权等待:进程不能进入临界区时,应释放处理器,避免忙等。

?#一个正在访问临界资源的进程由于申请等待I/O操作而被中断时,它可以允许其他进程抢占处理器,但是不能进入相关临界区。

2、用硬件实现中断

  • 中断禁用
  • 专用机器指令

3.信号量(☆☆☆☆☆)

  • 信号量:不要求忙等的同步互斥工具。
  • 一种信号量表示一种资源。信号量的值表示可用资源的个数。
  • 资源不可用时,进程将阻塞(避免忙等)(加入阻塞队列),直到另一进程释放资源时才被唤醒
  • 信号量s只能被下面的两个原语访问:

            #semWait(s)                也称作P(s)操作,wait(s)            ——本进程请求分配一个资源

            #semSignal(s)              也称作V(s)操作,signal(s)         ——本进程释放一个资源

        ·semWaits(s)、semSignal(s)操作必须成对出现。

        ·用于互斥时,位于同一进程内,临界区前后。

        ·用于同步时,交错出现于两个合作进程内。

        ·多个semWait()的次数不能颠倒,否则可能导致死锁。用于同步的semWait(s1)应出现在用于互斥的semWait(s2)之前

        ·多个semSignal()操作的次序可任意。

struct	semaphore {
	
	int count;
	queueType queue;
};

void semWait(semaphore s){
	s.count--;
	if(s.count < 0){
		/*把当前进程插入队列*/
		/*阻塞当前进程*/
	}
}

void semSignal(semaphore s){
	s.count++;
	if(s.count <= 0){
		/*把进程p从队列中移除*/
		/*把进程p插入就绪队列*/
	}
	
}

#用于互斥时,s的初值为1,取指为1~-(n-1)。

  •     s=1时:有一个临界资源可用,一个进程可进入临界区。
  •     s=0时:临界资源已分配,一个进程已进入临界区。
  •     s<0时:临界区已被占用,|s|个阻塞进程正在等待进入。

#用于同步时,s初值将>=0

  •     s>=0:可用资源个数(或可进入临界区的进程个数)。
  •     s<0    :  该资源的等待队列长度(阻塞进程个数)。

#强信号量:唤醒一个阻塞进程并移出阻塞队列时,采用FIFO移出策略的信号量。

#弱信号量:唤醒一个阻塞进程并移出阻塞队列时,采用随机选择移出策略的信号量。

#用信号量实现互斥:

const int n = /*进程数 */;
semaphore s =1;/*s的初值=1*/
void P(int i){	/*进程Pi*/
	while(true){
		semWait(s);
		/*临界区*/
		semSignal(s);
		/*其他部分*/
	}
}
void main(){/*n个进程并发*/
	parbegin(P(1),P(2),...,P(n));
}

#用信号量实现同步:

semaphore s =0;/*s的初值=0*/
Process	P1:
do{
	......
	代码段A;
	semSignal(s);
}
Process P2:
do{
	semWait(s)
	代码段B;
	......
}

4.生产者/消费者问题

  • 一组生产者进程和一组消费者进程共用一个有sizeofbuffer个缓冲区的缓冲池来交换数据(有限缓冲)
  • 资源,约束条件及信号量的设置

        #缓冲池一次只能让一个进程访问。(互斥

            设一信号量s,初值为1。

        #生产者需要空缓冲来发送数据。(同步

            设一信号量empty,初值为sizeofbuffer。

        #消费者需要满缓冲来获取数据同步

            设一信号量full,初值为0。

void producter(){
	while(true){
		semWait(empty);
		semWait(s);
		/*把数据送到缓冲区*/
		semSignal(s);
		semSignal(full);
	}
}

void consumer(){
	while(true){
		semWait(full);
		semWait(s);
		/*从缓冲区取出数据*/
		semSignal(s);
		semSignal(empty);
	}
}

5.读者/写者问题

有一个共享的数据对象:

  • 允许多个读者进程同时读
  • 一次只允许一个写者进程写;当一个写者正在写时,不允许其他任何读者或写者同时访问该共享对象。

1.读者优先

  • 当至少已有一个读者正在读时,随后的读者直接进入,开始读数据对象,但写者将等待。
  • 但一个写者正在写时,随后到来的读者和写者都等待。
  • 信号量的设置:

#一次只能让一个写者或一顿读者访问数据。

    设一互斥信号量wsem,初值为1。

#正在读数据的读者数目由全局变量readcount表示(初值=0),它被多个读者互斥访问。

(第1个读者需对数据加锁,最后1个读者对数据解锁)

           为readcount设一互斥信号量x,初值为1。

void reader(){
	while(true){
		semWait(x);
		readcount++;
		if(readcount == 1)
			semWait(wsem);
		semSignal(x);
		/*读数据对象*/
		semWait(x);
		readcount--;
		if(readcount == 0)
			semSignal(wsem);
		semSignal(x);
	}
}

void  writer(){
	while(true){
		semWait(wsem);
		/*写数据对象*/
		semSignal(wsem);
	}
}

2.写者优先

当一个写者声明想写时,不允许新的读者进入数据对象,只需要等到已有的读者读完即可开始写。可以避免写者饥饿

(具体实现略,有时间再补充)

6.管程

管程是由一个或多个过程、一个初始过程、一个初始化序列和局部数据组成的软件模块。主要特点如下:

  • 局部数据变量只能被管程的过程访问,任何外部过程都不能访问。
  • 一个进程通过调用管程的一个过程进入管程。
  • 在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。

(个人理解:管程就是将semWait()和semSignal()和共享数据封装起来,使得只能通过调用管程里的相应函数才能实现其功能。这样子做好处在于避免了进程有意/无意的互斥/同步错误。)


  • 条件变量(condition variable)

    #表示进程正在等待的资源或原因。只用于维护等待队列,但没有相关联的值。

  • 有两个函数可以操作条件变量:

    #cwait(c):条件不满足时,调用cwait(c)的进程被放在条件变量c的等待队列阻塞

    #csignal(c):唤醒一个条件变量为c的阻塞进程。

管程实现互斥/同步

7.消息

略(有时间再补充)


猜你喜欢

转载自blog.csdn.net/weixin_39793360/article/details/80648307