《现代操作系统》02章 进程与线程(一)

0 序言

  • 快要过年了,先祝福各位看到这篇博客的小伙伴新春快乐。

  • 进入第二章的学习,明显感觉到知识复杂难懂还很多,每天起不来床,还有许多其他的事情,距离上次更新只学习了三节的内容,先把这三节复习一下,开始过年了,年后继续学习和更新,O(∩_∩)O哈哈~

1 进程

1.1 进程模型

多道程序设计:(单核CPU)

  • CPU在各进程间来回切换
  • 任意时刻只有一个进程在运行
  • 进程运行时将其内存中的逻辑PC放入硬件PC
  • 进程结束或暂停时将硬件PC存入逻辑PC

每个进程执行其运算的速度不可确定,当再次运行时其运算速度也不可再现,进程编程不能对时序做确定假设

进程概念:

  • 计算机上可运行的软件(包括操作系统)
  • 一个正在执行的实例
  • 拥有自己的虚拟CPU

与程序的区别:

程序仅仅是进程操作的一部分’进程是取数据或状态、完成特定任务、存数据或状态等一系列动作的总和。(比如洗衣服和烧水都可以看做一个进程,人就像一个CPU,在洗衣服和烧水两个进程间切换,洗衣服时口渴,保存洗衣状态,去烧水,水壶放到火上,返回洗衣服,读取之前保存的洗衣状态继续执行,水烧好发出信号,暂停洗衣切换到烧水,如此往复)

1.2 创建进程

1.2.1 触发进程创建

系统初始化:

  • 前台:与人交互的进程
  • 后台:诸如电子邮件,新闻,打印类的守护进程

执行正在运行进程的进程创建系统调用:

新建进程以协助完成工作,如一个取数据一个处理数据,两个进程共享数据缓冲区

用户请求创建新进程:

键入命令或双击图标

批处理作业初始化:

大型批处理机中才会用到建

1.2.2 创建过程

Unix:

  • 调用fork(创建后父子进程有相同的存储映像)
  • 子进程可处理文件描述符,完成标准IO和错误重定向
  • 子进程执行ececve或类似系统调用修改存储映像

Windows(Win32)

  • 调用CreateProcess(10个参数,一步完成)

父子进程拥有不同的地址空间,Unix中子进程初始地址空间是父进程的副本,不可写的内存区共享,子进程还可共享父进程的一些其他资源。Windows子进程一旦创建就不再与父进程有任何关联。

1.3 进程终止

1.3.1 触发进程终止

正常退出(自愿)

  • 工作结束,调用exit(Unix)、ExitProcess(Win)
  • 图形界面中的X号,删除打开的临时文件并终止

出错退出(自愿)

  • 例如操作的文件不存在(命令行),并报错
  • 图形界面会提示重新输入

严重错误(非自愿)

  • 进程(程序)引起的错误:执行非法指令、引用不存在内存、除数是0等等
  • 有些进程通知操作系统,自己可以处理这些错误,这时进程收到的是错误信号,而不是终止命令

被其他进程杀死(非自愿)

  • 一个进程杀死另一个进程,Kill(Unix),Terminate-Process(Win)
  • 杀手进程要有权限许可

1.3.2 注意

有些系统中,一个进程终止,其创建的进程也随之终止,但Unix和Win都不是这种机制

1.4 进程层次结构

Unix:

  • 一个进程与其所有后代进程构成进程组
  • 例如:键盘发出信号,此信号传递给与键盘相关的进程组的所有成员,每个进程可以选择捕获、忽略、默认(被杀死)

Win:

  • 没有进程组的概念,众生平等
  • 进程创建时,父进程得到一个令牌(控制句柄),可以控制子进程,但是令牌可以被转交,因此层级关系被打破

1.5 进程状态与转换

阻塞态:

  • 进程在逻辑上无法运行时被阻塞
  • 由进程自身引起
  • 例如:两个进程协同,一个进程的输出为另一个进程的输入,当第一个进程工作未完成时第二个进程就要阻塞
  • 例如:等待一个中断信号,中断信号到来前处于阻塞态,中断信号到来后切换到就绪态

就绪态:

  • CPU进程间切换时被迫停止的进程(若不切换仍可运行)
  • 由系统引起,或从阻塞态切换到就绪态

运行态:

-进程占用CPU并运行

状态转换:
进程状态转换
进程模型:
进程模型

1.6 进程实现

进程表:(Process table)

  • 操作系统维护进程表(一个数据结构)
  • 每个进程占用一个进程表项(进程控制块):
    – 包含程序计数器、堆栈指针、内存分配情况
    – 打开的文件状态、账号、调度信息
    – 进程状态转换时必须保存的信息

中断:

中断发生(PC、PSW、一些寄存器被硬件程序压入堆栈)
硬件将中断向量放入PC
保存当前进程的寄存器(放入进程控制块)
删除由硬件程序存入堆栈的信息
堆栈指针指向进程处理程序所使用的临时堆栈
(上面3步均需要汇编代码完成,该段代码基本所有进程共用)
调用相应的中断服务函数
调用调度程序,决定让哪个(已经就绪的)进程运行
为当前进程装入寄存器值及内存映射,开始运行(汇编)

1.7 多道程序设计模型

CPU利用率:

  • 1-P^n(p:进程I/O操作时间与内存停留时间之比,n:内存中进程数)
  • 合理的增加内存可以提高CPU利用率,当然要考虑性价比

2 线程

线程是进程中并发的(多线程下)顺序执行的单元
传统操作系统中,每个进程有一个地址空间和一个控制线程
线程可以理解为进程中的迷你进程
线程才是执行的实体,进程是一个宏观的概念

2.1 线程的使用

一个应用中同时发生多种活动

– 将这些活动分解为可以准并行的多个顺序线程,简化程序设计模型
– 并行实体共享同一地址空间和所有可用数据(多进程无法共享地址空间)

线程比进程更轻量级

-比进程更容易创建、撤销(10-100倍)

提高性能

  • 当多个线程中有大量IO和计算任务,允许多个线程的活动重叠进行,加快执行速度
  • 若是CPU密集型(计算较多)则没有优势
  • 在多核处理器上运行,实现真正的并行

举例1 文档处理进程

线程1:与用户交互
线程2:页面排版
线程3:周期性保存

举例2 Web服务器的实现方式

多线程处理:

分派线程从网络获得请求并检查
选取一个空转(处于阻塞态)的工作线程提交请求
工作线程由阻塞态进入就绪态
检查请求是否在高速缓存中
不存在,调用读磁盘操作,并阻塞(等待读磁盘完成)
存在,返回页面信息,并阻塞(等待新的请求)
分派线程继续检查和分派

在这里插入图片描述

单线程处理:

获取请求
检查缓存或读磁盘(阻塞)
返回页面
(在读磁盘时CPU空转,不接受其他请求)

(3)非阻塞的单线程(有限状态机)

1)获取请求
2)检查缓存:
- 存在:返回页面
- 不存在:
- —读磁盘
- —保存当前请求状态到表格(存放事件状态
3)处理下一事件:(使状态发生改变的事件集合
- 新的工作请求:跳转第一步
- 磁盘应答(中断信号):
- —从表格中读取对应请求信息
- —返回该请求的页面
- —跳转第一步

2.2 经典的线程模型

2.2.1 单线程模型

进程中仅有一个控制线程

2.2.2 多线程模型

线程间资源共享又彼此独立:
在这里插入图片描述

线程间共享进程资源——一家人拥有的共同财产
线程间没有保护——因为都是自家人,自家人要互帮互助共同协作(线程间可以修改堆栈等信息,但道德上说不过去)
线程拥有自己的资源——每个人拥有自己的私有财产
每个线程要调用自己的过程,记录过程调用的局部变量、地址、状态等

多线程的工作模式:

在这里插入图片描述

– 通常都是单个线程开始工作
– 还有一些调用允许某个线程等待另一个线程完成某些任务,或等待一个线程宣称它已经完成了相关工作等。
– 多线程的设计要考虑到线程间的配合问题,防止一件事做两次,或其他不应该发生的现象。

2.3 POSIX线程

Pthread(线程标准)(部分)

调用 描述
Pthread_create 创建新线程
Pthread_exit 结束调用的线程
Pthread_join 等待特定的线程退出
Pthread_yield 释放CPU以运行其他线程
Pthread_attr_init 创建并初始化一个线程的属性结构
Pthread_attr_destroy 删除一个线程的属性结构(释放内存,线程依然存在)

2.4 在用户空间中实现线程

线程包放在用户空间
内核按照进程方式管理——在内核看来是单线程进程,但在进程内部实现了多线程
可以在不支持线程的系统上实现——线程的切换调度由用户进程调用相应的函数库来

用户空间实现线程

运行时系统暂且可以理解为类似Java、Python运行环境的进程,他们都提供了线程调度的库实现

优点:

快速——只需要修改CPU中寄存器、堆栈指针、程序计数器就可以实现线程间切换,不需要陷入内核、不需要陷阱、不需要上下文切换、不需要对内存高速缓存刷新
灵活——允许每个运行时系统有自己的线程调度算法
较好的扩展性——内核中不能有过多的线程,而用户空间可以

缺点:

阻塞系统调用——若 一个线程阻塞,会使整个进程阻塞(因为系统只认为这是一个单线程进程),这样其他就绪的进程就被迫无法运行,有一个解决办法是,在执行阻塞系统调用时先用 包装器(检查这个系统调用是否会引起阻塞) 对该调用进行安全认证,不引起阻塞再执行,否则切换其他线程执行
页面故障——跳转到了一条不在内存中的指令,操作系统去磁盘读取丢失的指令(和它的邻居们),若页面故障由某个线程引起,但系统只认为这是一个单线程进程,就把整个进程阻塞了
线程调度困难——用户态的进程内没有时钟中断,不能实现轮转调度,只能是某个线程自愿交出CPU的使用权
违背初心——程序员希通常在经常发生线程阻塞的应用中使用多线程模型,但是用户态的多线程模型又极力避免阻塞调用

2.5 在内核中实现线程

特性:

线程表放在内核中——有相应的系统调用完成线程的创建和撤销
线程的调度由内核决定——当一个线程阻塞,内核可以调用同进程中就绪的线程,也可以调用其他进程中就绪的线程
内核中线程创建撤销代价大——使用线程回收机制,当一个线程需要撤销时,将其标记为不可运行(仍然保留其数据结构),当随后要创建线程时,再把这个旧线程复活
克服阻塞系统调用和页面故障的问题——在出现阻塞调用和页面故障时,内核可以调用本进程中其他就绪线程或其他进程就绪线程(同第二点)

内核中实现线程
带来的问题:

  • 多线程进程创建新进程是复制所有进程还是保留一个?
  • 当进程收到信号时应该交给哪个线程?当多个线程注册了某个信号,这些线程都需要响应么?
  • 内核线程的速度慢。

2.6 混合实现线程

采用多路复用的思想,也就上将上两种思想进行混合

混合实现

2.7 调度程序激活机制

在用户空间实现线程调度

2.7.1 上行调用

  • 由于内核线程调度慢,几个牛人又想出了一种解决之道——上行调用(upcall),一般而言n层为n+1层提供调用,而n层不能调用n+1层,但上行调用违反了这一原则,允许下层对上层的调用,这样避免了在用户空间和内核空间的不必要转换,提高了效率。
  • 内核为每个进程提供一些虚拟CPU(按需分配,进程可申请可退还,内核可分发可回收),进程可以自主的把线程分配到虚拟CPU上,这些虚拟CPU可能会成为真实的CPU。

2.7.2 阻塞处理

发出阻塞通知:

内核了解到某个线程阻塞,在一个已知的起始地址启动运行时系统,通知该进程的运行时系统,在堆栈中传递阻塞线程的编号和事件描述。

重新调度:

运行时系统被激活,开始调度线程
将当前进程标记为阻塞
从就绪表中取出另一线程,设置寄存器,启动

2.7.3 就绪处理

发出就绪通知:

内核了解到阻塞的线程可以就绪
上行调用运行时系统,并通知就绪信息

重新调度:

选择立即调度,或把阻塞改为就绪(稍后执行)

2.7.4 中断处理

硬件中断发生,被中断的CPU进入核心态

感兴趣中断

  • 被中断进程对该信号感兴趣(进程中的某个线程的IO完成或页面到达),被中断的线程被挂起,状态被保存到堆栈
  • 运行时系统启动对应的虚拟CPU,并决定调用哪个线程(被中断的、就绪的、或者其他)

不感兴趣中断

  • 被中断进程对该信号不感兴趣(其他进程的IO完成或页面到达),则去处理中断程序
  • 中断程序执行完成,恢复来的线程状态

2.8 弹出式线程

传统方式: 将消息处理线程阻塞到一个receive系统调用,等待消息的到来
弹出式: 在消息到来时,创建一个新的线程用于处理该信息,由于没有历史(必须存储的寄存器或堆栈),创建速度很快,在分布式系统中常见
优点: 若在内核中运行,方便快捷,容易访问表格和IO设备
缺点: 出现错误后损害较大(某个线程运行时间过长而无法抢占,引起信息丢失)

2.9 单线程代码多线程化

2.9.1 全局变量管理

原来的程序是单线程进程,全局变量处处可见
把单线程进程程序变为多线程进程程序,若全局变量仍处处可见就会发生问题

解决方法:

  • 禁止全局变量(一般不太可行)
  • 每个线程拥有私有的全局变量
    引入新的库过程,用于创建、设置和读取线程范围内的全局变量,同名的变量分处在不用的存储区域(这些存储区是为线程划定的)
    为每一个过程提供一个包装器,包装器设置一个二进制位以标致这个库正在被某一线程使用,其他要调用这个库的线程会被阻塞,但这种方法效率低

2.9.2 信号管理

书中只是说很复杂,并没有具体的解决方法

2.9.3 堆栈管理

依然非常复杂,解决非常费力

2.9.4 总结

给已有的系统引入线程而不进行实质性的系统重新设计是不行的,如重新定义系统调用语义,重写库,并且兼容单线程进程

3 进程间通信

研究的三个问题:

一个进程如何把信息传递给另一个进程
两个或更多进程在关键活动不出现交叉(例如抢票系统)
进程间要有正确的顺序

解决方法同样适用于线程间通信

3.1 竞争条件

以一个打印程序为例,打印程序负责打印存放在打印槽内的目录所指向的文件,打印后清空对应的槽并修改out(下一个要打印的槽)值。请求打印的进程要把打印内容所在目录放在空的打印槽上,并修改in(下一个空槽)值,此时A、B进程都请求打印,发生了如下情况(现在假设7-10槽空,即in=7)

竞争条件

两个或多个进程读写某些(存放在共享内存空间)共享数据,而最后的结果取决于精确地时序——竞争条件
大多数情况下运行良好,但极少数情况发生无法解释的奇怪现象——我称之为玄学

3.2 临界区

进程内使用共享内存的程序片段——临界区

避免竞争条件:

任何两个进程不能同时处于临界区(互斥
不应对CPU的速度和数量做任何假设
临界区外运行的进程不得阻塞其他进程
不得使进程无限期等待进入临界区

解决方法

3.3 互斥的实现

屏蔽中断

进入临界区关闭中断,即将离开时开启中断

只能用于单CPU,并且存在卡死和崩溃的情况

锁变量

0代表无进程在临界区,1代表有进程在临界区

同样会出现竞争条件,不可取

严格轮转法 (忙等待)

进程0代码

while(1)
{
    
    
	while(turn!=0);//忙等待
	critical_region();//执行临界区代码
	turn = 1;//允许进程1进入临界区
	noncritical_region;//执行非临界区代码
}

进程1代码

while(1)
{
    
    
	while(turn!=1);//忙等待
	critical_region();//执行临界区代码
	turn = 0;//允许进程0进入临界区
	noncritical_region;//执行非临界区代码
}

进程0和进程1在临界区不停地轮转,要求临界区的代码能快速执行,忙等待的时间不宜过长,若一个进程临界区允许过慢会阻塞另一个进程,这违背了避免竞争条件的要求

仍然不是一个很好的解决之道

Peterson解法 (忙等待)

#define FALSE 0
#define TRUE 1
#define N 2 //进程数

int turn;//正在轮转的进程号
int interested[N];//所有初始化值为0

void enter_region(int process)//进程0或1进入临界区前调用
{
    
    
	int other;//其他进程号
	
	other = 1-process;
	interested[process] = TRUE;//希望进入临界区
	turn = process;
	while (turn == process && interested[other] == TRUE);//确认当前进程可以进入临界区,若不可以则忙等待
}

void leave_region(int process)//进程0或1退出临界区前调用
{
    
    
	interested[process] = FALSE;
}

是一个切实可行的算法

TSL指令(Test and Set Lock)

需要硬件支持

TSL RX,LOCK //汇编指令

指令执行期间,CPU锁住存储总线

//汇编代码
enter_region:
	TSL REGISTER,LOCK	|复制锁到寄存器并置LOCK为1
	CMP REGISTER,#0		|锁是0?(没有被锁?)
	JNE enter_region	|不是0就已经被锁了,挂起
	RET					|返回调用者,进入临界区

leave_region:
	MOVE LOCK,#0		|将锁置0(解锁)
	RET					|返回调用者,退出临界区

JNE:Jump if Not Equal

XCHG指令

TSL的替代指令

//汇编代码
enter_region:
	MOVE REGISTER,#1	|寄存器放1
	XCHG REGISTER,LOCK	|交换寄存器和锁的值
	CMP REGISTER,#0		|锁是0?(没有被锁?)
	JNE enter_region	|不是0就已经被锁了,挂起
	RET					|返回调用者,进入临界区

leave_region:
	MOVE LOCK,#0		|将锁置0(解锁)
	RET					|返回调用者,退出临界区

3.4 睡眠与唤醒

Peterson和TSL有忙等待的缺点

当进程间有优先级时,会出现优先级反转——低优先级进程在高优先级进程就绪时不被调用,低优先级进程卡在临界区无法退出,导致高优先级进程一直忙等待

原语:

sleep——进程挂起(引起进程阻塞的系统调用)
wakeup——唤醒某个进程(解除挂起)

生产者-消费者问题

也称为有界缓冲区问题
两个进程共享公共固定大小缓冲器

生产者——检查count,未满,写数据,满,睡眠
消费者——检查count,未空,读数据,空,睡眠

#define N 100 //缓冲区大小
int count = 0; //缓冲区中的消息数量

void producer(void)
{
    
    
	int item;
	
	while(1)
	{
    
    
		item = produce_item(); //生产消息
		if(count == N) sleep(); //缓冲区满则睡眠
		insert_item(item); //缓冲区未满,放入消息
		count += 1; 计数器自增
		if(count == 1) wakeup(consumer); //有数据时唤醒消费者
	}
}

void consumer(void)
{
    
    
	int item;
	
	while(1)
	{
    
    
		if(count == 0) sleep(); //缓冲区空则睡眠
		item = remove_item(); //缓冲区未满则读取消息
		count -= 1; //计数器递减
		if(count == N-1) wakeup(producer); //有空余唤醒生产者
		consum_item(item); //输出消息
	}
}

缺点: wakeup发给清醒的进程时,wakeup丢失,导致最终两个进程都永久休眠。

不优雅的解决方法: 为wakeup建一个仓库——wakeup等待位,当被唤醒进程清醒时该位置1,当此进程将要睡眠时,该位置0,仍然保持清醒

3.5 信号量

3.5.1 信号量

使用一个整形变量n(信号量)累积唤醒次数

两个原子操作: down、up(运行时不能被其他打断

down up

3.5.2 信号量解决生产-消费问题

将up、down以系统调用的方式实现

#define N 100 //缓冲区大小
int mutex = 1; //控制临界区访问
int empy = N; //空槽数
int full = 0; //有消息的槽数

void producer(void)
{
    
    
	int item;
	
	while(1)
	{
    
    
		item = produce_item(); //生产消息
		down(&empty); //空槽数减一
		down(&mutex); //进入临界区
		insert_item(item); //缓冲区未满,放入消息
		up(&mutex); //离开临界区
		up(&full); //满槽数加一
	}
}

void consumer(void)
{
    
    
	int item;
	
	while(1)
	{
    
    
		down(&full); //满槽数减一
		down(&mutex); //进入临界区
		item = remove_item(); //缓冲区未满则读取消息
		up(&mutex); //离开临界区
		up(&empty); //空槽数加一
		consum_item(item); //输出消息
	}
}

信号量:

full、empty: 用于同步,full=0消费者sleep,empty=0生产者sleep
mutex: 用于互斥,mutex=0当前进程sleep

3.6 互斥量

3.6.1 用户级线程互斥

是信号量的简化版,mutex的值仅取0和1
进入临界区调用 mutex_lock,退出临界区调用 mutex_unlock
一般用在 用户线程 中使用

//汇编代码
mutex_lock:
	TSL REGISTER,MUTEX	|复制互斥量到寄存器并置MUTEX为1
	CMP REGISTER,#0		|锁是0?(没有被锁?)
	JE/JZ ok			|没有被锁
	CALL thread_yield	|交出CPU
	JMP mutex_lock		|再次检查锁
ok:RET					|返回调用者,进入临界区

mutex_unlock:
	MOVE MUTEX,#0		|将锁置0(解锁)
	RET					|返回调用者,退出临界区

3.6.2 Pthread中的互斥

函数名 描述
pthread_mutex_init 创建化一个互斥量
pthread_mutex_destroy 销毁一个互斥量
pthread_mutex_lock 同mutex_lock
pthread_mutex_unlock 同mutex_unlock
pthread_mutex_trylock 尝试加锁,成功或失败

3.6.3 Pthread中的条件变量

另一种同步机制,双保险

函数名 描述
pthread_cond_init 创建化一个条件变量
pthread_cond_destroy 销毁一个条件变量
pthread_cond_wait 阻塞线程,等待一个信号,原子性调用并解锁它的互斥变量
pthread_cond_signal 向另一个线程发信号以唤醒
pthread_cond_broadcast 向多个线程发信号将他们全部唤醒

条件变量不会存在内存中,若信号传递给一个没有线程在等待的条件变量,信号会丢失,需谨慎使用

例程:

#include <stdio.h>
#include <pthread.h>

#define MAX 1000000000
pthread_mutex_t the_mutex;
pthread_cond_t condc,condp;
int buffer = 0;

void *producer(void *ptr)
{
    
    
	int i;
	
	for(i=1;i<=MAX;i++)
	{
    
    
		pthread_mutex_lock(&the_mutex); // 检查锁,锁住缓冲区或睡眠
		while(buffer != 0) pthread_cond_wait(&condp,&the_mutex); // 等待consumer把数据读走
		buffer = i; // 数据放入缓冲区
		pthread_cond_signal(&condc); // 缓冲区中有内容,给consumer发信号
		pthread_mutex_unlock(&the_mutex); // 释放缓冲区
	}
	pthread_exit(0);
}

void *consumer(void *ptr)
{
    
    
	int i;
	
	for(i=1;i<=MAX;i++)
	{
    
    
		pthread_mutex_lock(&the_mutex); // 检查锁,锁住缓冲区或睡眠
		while(buffer == 0) pthread_cond_wait(&condc,&the_mutex); // 等待producer把数据放入
		buffer = 0; // 清空缓冲区
		pthread_cond_signal(&condp); // 缓冲区无内容,给producer发信号
		pthread_mutex_unlock(&the_mutex); // 释放缓冲区
	}
	pthread_exit(0);
}

int main(int argc,char **argv)
{
    
    
	pthread_t pro,con; // 两个线程 生产者,消费者
	pthread_mutex_init(&the_mutex,0); // 初始化mutex
	pthread_cond_init(&condc,0); // 初始化条件变量
	pthread_cond_init(&condp,0);
	pthread_create(&con,0,consumer,0); // 创建线程
	pthread_create(&pro,0,producer,0);
	pthread_join(pro,0);
	pthread_join(con,0);
	pthread_cond_destroy(&condc); // 销毁条件变量
	pthread_cond_destroy(&condp);
	pthread_mutex_destroy(&the_mutex); // 销毁互斥变量
}

3.7 管程

当down的使用顺序不正确时还可能会出现死锁(两个进程都被永远阻塞),为了更简单的实现同步,管程(monitor) 出现了。

管程在同一时间只允许一个进程活跃,是由编译器完成的互斥,编译器知道对管程的调用是特殊的,在进入管程前先检查是否有其他进程在管程中活跃

3.7.1 Pascal表示

管程的结构示例:

monitor example
	integer i;
	condition c;
	
	procedure producer();
	.
	.
	.
	end;
	
	procedure consumer();
	.
	.
	end;
end monitor;

类Pascal的生产-消费例程

monitor ProducerConsumer
	condition full,empty;
	integer count;
	
	procedure insert(item,integer);
	begin
		if count = N then wait(full);
		insert_item(item);
		count:= count + 1;
		if count = 1 then signal(empty);
	end;
	
	function remove:integer;
	begin
		if count = 0 then wai(empty);
		remove = remove_item;
		count := count - 1;
		if count = N - 1 then signal(full);
	end;
	
	count := 0;
end monitor;

procedure producer:
begin
	while true do
	begin
		item = produce_item;
		ProducerConsumer.insert(item);// 调用时不会被打断
	end;
end;

procedure consumer:
begin
	while true do
	begin
		item = ProducerConsumer.remove; // 调用时不会被打断
		consume_item(item);
	end;
end;

3.7.2 Java实现

Java用管程实现生产-消费问题

java可以实现管程:

public class ProducerConsumer
{
    
    
	static final int N = 100; // 缓冲区大小
	static producer p = new producer(); // 初始化一个新的生产者线程
	static consumer c = new consumer(); // 初始化一个新的消费者线程
	static our_monitor mon = new our_monitor(); // 初始化一个新的管程
	
	public static void mian(String args[])
	{
    
    
		p.start(); // 开始生产者线程
		c.start(); // 开始消费者线程
	}
	
	static class producer extends Thread
	{
    
    
		public void run()// 线程代码
		{
    
    
			int item;
			while(true)// 生产者循环
			{
    
    
				item = produce_item();
				mon.insert(item);
			}
		}
		private int produce_item()
		{
    
    
			// 生产者函数体
		}
	}
	
	static class consumer extends Thread
	{
    
    
		public void run()// 线程代码
		{
    
    
			int item;
			while(true)// 消费者循环
			{
    
    
				item = mon.remove();
				consume_item(item);
			}
		}
		private void consume_item()
		{
    
    
			// 消费者函数体
		}
	}
	
	static class our_monitor // 一个管程
	{
    
    
		private int buffer[] = new int[N];
		private int count = 0,lo = 0,hi = 0; // 计数器和索引
		
		public synchronized void insert(int val)
		{
    
    
			if(count == N) go_to_sleep(); // 缓冲区满,进入休眠
			buffer[hi] = val; // 向缓冲区中插入数据
			hi = (hi + 1)%N; // 设置下一个插入的位置
			count += 1; // 缓冲区内数据量自增
			if(count == 1) notify(); // 有数据,若消费者睡眠则唤醒它
		}
		
		public synchronized int remove()
		{
    
    
			int val;
			if(count == 0) go_to_sleep(); // 缓冲区空进入睡眠
			val = buffer[lo]; // 从缓冲区中取走一个数据
			lo = (lo + 1)%N; // 设置下一个取数据位置
			count -= 1; // 缓冲区内数据量递减
			if(count == N-1) notify(); // 缓冲区未满,若生产者睡眠则唤醒它
			return val;
		}
		private void go_to_sleep()
		{
    
    
			try{
    
    wait();}
			catch(IterruptedException exc){
    
    };
		}
	}
}
  • 若一个分布式系统有多个CPU,每个CPU有自己的私有内存(没有共享的地址空闲),这些方法都会失效
  • 信号量太低级
    管程只在少数语言中可以实现
    这些方法都没有提供机器间的信息交换方法
    我们还需哟其他更高级的方法——消息传递

3.8 消息传递(message passing)

原语: send、receive

类似信号量,是系统调用

send(destination,&message);
receive(source,&message);

3.8.1 消息传递设计要点

不可靠消息传递中的成功通信问题:

使用ack机制
数据包编号
检查包重复

进程命名问题:

有唯一的ID
不可被仿造

通信速度问题:

同一台机器,消息传递要不信号量、管程慢

3.8.2 消息传递解决生产-消费问题

系统中总的消息量不变——消费者将空的缓冲槽发给生产者,生产者把填充好消息的缓冲槽发送给消费者

#define N 100

void producer(void)
{
    
    
	int item;
	message m; // 消息缓冲区
	
	while(1)
	{
    
    
		item = produce_item(); // 生产者放入缓存中一些数据
		receive(consumer,&m); // 等待消费者发送新的缓冲区
		build_message(&m,item); // 建立一个待发送的消息
		send(consumer,&m); // 发送数据给消费者
	}
}

void consumer(void)
{
    
    
	int item,i;
	message m; // 消息缓冲区
	
	for(i=0;i<N;i++) send(producer,&m); // 发送N个空的缓冲区
	while(1)
	{
    
    
		receive(producer,&m); // 接收包含数据的消息
		item = extract_item(&m); // 将数据从消息中提取出来
		send(producer,&m); // 将空缓冲槽发给生产者
		consume_item(item); // 处理数据
	}
}
  • message 可以是一种新的数据结构——邮箱,每个邮箱都有自己唯一的地址
  • 当不使用缓冲,只有send之后才会receive,只有receive之后才会send,其他情况会阻塞,这种方法易实现,灵活性低
  • 通常并行程序设计系统中使用消息传递,例如消息传递接口(Message Pussing Interface,MPI)

3.9 屏障

用于进程组的同步管理
将运行过程分为不同的阶段
当指定的某些进程都完成这一阶段时,才可开启下一阶段

原语: barrier

举个例子

一个走廊分为很多段(阶段),每段之间有个大门(屏障)有m把锁(钥匙不同),当m把锁都打开才能通过,有m个管理员(进程),每个管理员有一把钥匙,可以打开m把锁中的其中一个,只有当m个管理员(进程)都做完自己的工作来到大门前,才能打开大门进入下一个隔断(阶段),当最后一个管理员没有完成工作时,其他管理员要在大门处等候(调用barrier将自己阻塞)。

应用:

一个数学问题,要对一个庞大的矩阵进行某种迭代,把矩阵分为不同的块,每个进程负责一个块的迭代,只有所有的进程都完成n次迭代,才能进行n+1次迭代

觉得不错点个赞收藏一下,欢迎交流学习,这里是海小皮,我们一同进步!!!

猜你喜欢

转载自blog.csdn.net/weixin_42464904/article/details/113781720