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(运行时不能被其他打断)
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次迭代
觉得不错点个赞收藏一下,欢迎交流学习,这里是海小皮,我们一同进步!!!