【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题

本次笔记内容:
10.6 经典同步问题

文章目录

    • 【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题
        • 问题分析
        • 信号量代码实现(读者优先)
        • 信号量代码实现(写者优先)
        • 使用管程实现写者优先
          • 伪码表示
          • 管程设计
      • 哲学家就餐问题
        • 问题描述
        • 幼稚的解决方案
          • 直观的解决办法
          • 稍改进后的方案
          • 等待时间随机的方案
          • 简单的互斥访问方案
        • 正确的解决方案
          • 思路1:哲学家自己怎么解决这个问题?
          • 思路2:计算机怎么解决这个问题?
          • 思路3:怎么样来编写程序?
            • 事前准备
            • 函数take_forks的定义
            • 函数test_take_left_right_forks的定义
            • 函数put_forks的定义
        • think()与eat()是否需要进一步实现?
      • 总结


### 读者-写者问题 #### 问题描述 对于一段数据,不允许边写边读。并且由于读操作对于数据不会有破坏,因此允许对一段数据进行并行读操作。

动机:

  • 共享数据的访问

两种类型使用者:

  • 读者:不需要修改数据
  • 写者:读取和修改数据

问题的约束:

  • 允许同一时间有多个读者,但在任何时候只能有一个写者;
  • 当没有写者时读者才能访问数据;
  • 当没有读者和写者时写者才能访问数据;
  • 在任何时候只能有一个线程可以操作共享变量。

问题分析

多个并发进程的数据共享:

  • 读者 - 制度数据集:他们不执行任何更新;
  • 写者 - 可以读取和写入。

共享数据:

  • 数据集
  • 信号量CountMutex初始化为1(对Rcount进行互斥保护)
  • 信号量WriteMutex初始化为1(对写者进行互斥保护)
  • 整数Rcount初始化为0(代表当前多少读者)

读者优先:

  • 写者等待时,又来了一个读者,则读者跳过写者,先去操作。

信号量代码实现(读者优先)

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第1张图片

上图中:

  • sem_wait(WriteMutex)相当于对WriteMutex做p操作;
  • sem_post(WriteMutex)相当于v操作;
  • WriteMutex确保了写者的互斥性;
  • Rcount记录当前读者的个数,如果等于0,意味着没有一个读者;
  • ++Rcount和–Rcount分别在read上下;
  • –Rcount后有if(Rcount == 0) sem_post(WriteMutex);检验是否还有写者在等待;
  • 使用sem_wait()和sem_post()将Rcount包起来,保证不会有多个读者线程同时进行Rcount的++或–。

信号量代码实现(写者优先)

等待的写者不会一直处于阻塞状态:一旦写者就绪,其在队列中出队优先级一直高于读者。

  • 作为作业,日后实现。

使用管程实现写者优先

读者需要等待:当前在写操作的写者与等待中的写者。

伪码表示
Basic structure:two methods

Database::Read() {
     
	Wait until no writers;
	read database;
	check out - wake up waiting writers;
}

Database::Write() {
     
	Wait nutil no readers/writers;
	write database;
	check out - wake up waiting readers/writers;
}
管程设计

Monitor’s State variables:

AR = 0;	// # of active readers
AW = 0;	// # of active writers
WR = 0;	// # of waiting readers
WW = 0;	// # of waiting writers
Condition okToRead;
Condition okToWrite;
Lock lock;

逐步细化地实现。

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第2张图片

  • Lock.Acquire()和lock.Release()包起整个命令,保证只有一个管程在临界区;
  • while((AW+WW)>0){}进行判断,如果有写者,WR++,表示当前有多了一个在等待的写者;
  • 对于DoneRead(),当执行完Database()的read之后,唤醒处于等待状态的写者;
  • if(AR == 0 && WW > 0){}表示,如果还有读者在操作,并且有写者在等待,则唤醒一个写者。

对于写者:

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第3张图片

  • 对于StartWrite(){},while(AW+AR > 0){}用于判断当前临界区是否为空;
  • 在DoneWrite()中,可以看到,是先用if(WW > 0){}判断是否有等待状态的写者;然后爱判断是否有读者等待;
  • 注意,signal()是唤醒一个;broadcast()则是唤醒条件变量上所有线程。

哲学家就餐问题

问题描述

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第4张图片

上图为问题描述,与数据结构定义。

幼稚的解决方案

直观的解决办法

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第5张图片

使用最简单、最直观的解决方式:如果5个人同时拿起左边叉子,大家都准备拿起右边叉子时,出现死锁。

稍改进后的方案

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第6张图片

上图方案为每个哲学家增加了判断机制。问题在于,可能循环:大家同时拿起左边叉子,又同时方下,如此循环。

等待时间随机的方案

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第7张图片

基于之前的判断方案,上图方案为每个哲学家增加一个随机等待时间。但不完美。

简单的互斥访问方案

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第8张图片

上述方案的缺点与思考:

  • 它把就餐(而不是叉子)看成是必须互斥访问的临界资源,因此会造成(叉子)资源的浪费;
  • 从理论上说,如果有五把叉子,应允许两个不相邻的哲学家同时就餐。

正确的解决方案

思路1:哲学家自己怎么解决这个问题?

指导原则:要么不拿,要么就拿两把叉子。

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第9张图片

动作如上图。

思路2:计算机怎么解决这个问题?

指导原则:不能浪费CPU时间;进程间相互通信。

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第10张图片

思路3:怎么样来编写程序?
  1. 必须有数据结构,来描述每个哲学家当前状态;
  2. 该状态是一个临界资源,对它的访问应该互斥地进行;
  3. 一个哲学家吃饱后,可能要唤醒邻居,存在同步关系。
事前准备

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第11张图片

上图中,对于状态的定义,使用枚举。

【操作系统/OS笔记14】经典同步问题:读者-写者问题、哲学家就餐问题_第12张图片

上图philosopher的定义中,重点在于take_forks()与put_forks()的实现。

函数take_forks的定义
// 功能:要么拿到两把叉子,要么被阻塞起来
void take_forks(int i)	// i的取值:0到N-1
{
     
	P(mutex);						// 进入临界区
	state[i] = HUNGRY;				// 我饿了!
	test_take_left_right_forks(i);	// 试图拿两把叉子
	V(mutex);						// 退出临界区
	P(s[i]);						// 没有叉子便阻塞
}

其中,P(mutex)与V(mutex)将其包含的操作保护起来。

函数test_take_left_right_forks的定义
void test_take_left_right_forks(int i)	// i取0到N-1
{
     
	if(state[i] == HUNGRY &&
		state[LEFT] != EATING &&
		state[RIGHT] != EATING)
	{
     
		state[i] = EATING;	// 两把叉子到手
		V(s[i]);			// 通知第i人可以吃饭了(通知自己)
	}
}

此处V(s[i])操作与take_forks()中的P(s[i])操作对应,使P操作不会被阻塞;如果不满住if()条件,则V操作没有执行,P操作会被阻塞。那P将在什么时候唤醒?其左右邻居中有人实现put_forks时可能唤醒。

函数put_forks的定义
// 功能:把两把叉子放回原处,并在需要的时候唤醒左右邻居
void put_forks(int i)	// i的取值:0到N-1
{
     
	P(mutex);				// 进入临界区
	state[i] = THINKING;	// 交出两把叉子
	test_take_left_right_forks(LEFT);	// 看左邻居能否进餐
	test_take_left_right_forks(RIGHT);	// 看右邻居能否进餐
	V(mutex);							// 退出临界区
}

think()与eat()是否需要进一步实现?

eat()不需要进一步实现,eat()对应的就是临界区。

think()需要在初始时置为thinking,且需要pv操作。

总结

很多同步互斥问题都有难度,在本次课程的两个实现中,我们的方法流程如下:

  1. 一般人怎么解决这个问题,写出伪代码;
  2. 伪代码化为程序流程;
  3. 设定状态变量,确定包含起来互斥与否,使用信号量和管程等哪种手段;
  4. 逐步细化方式,完成整个处理过程。一般来讲,申请资源、释放资源都是匹配的。

你可能感兴趣的:(计算机基础,操作系统)