并发的同步互斥与死锁
程序员文章站
2022-05-22 11:20:45
...
并发进程
程序执行的顺序性
- 内部顺序性:一个进程内部语句的执行是顺序的,只有当一个操作结束后,才能开始后继操作
- 外部顺序性:多个进程间的顺序执行关系,这些进程在时间上按调用次序严格有序执行
程序执行的并发性
- 进程的并发性是指一组进程的执行在时间上是重叠的,即一个进程执行的第一条指令是在另一个进程执行的最后一条指令完成之前开始的
- 优点:能够同时启动多台设备操作,充分利用处理器与外围设备、外围设备与外围设备之间的并行工作能力,减少设备间的等待,提高资源利用率和计算机的工作效率
- 分类
- 无关并发进程:一组并发进程分别在不同的变量集合上操作,一个进程的执行与其他并发进程的进展无关,即一个并发进程不会改变另一个并发进程的变量值
- 交互并发进程:一组并发进程共享某些变量,一个进程的执行可能影响其他并发进程的执行结果
与时间有关的错误
- 结果不唯一,如抢票
- 永远等待:当合作进程之间等待、唤醒之类的同步信号发送次序颠倒时,等待进程因错过了唤醒信号而永远等待
进程的协作和竞争
- 竞争:由于并发进程共用一套计算机系统资源引起的,一个进程的执行可能影响与其竞争资源的其它进程
- 死锁:进程直接或间接互相等待对方的资源
- 饥饿:一些进程总是优先于另一些进程,通过FCFS解决
- 互斥:若干进程要使用同一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源。是解决进程竞争关系的手段,通过临界区解决互斥问题
- 协作:某些并发进程为完成同一任务而共享某些数据,形成协作关系
- 同步:协作进程之间需要同步,即一个进程在接收到伙伴进程发来的信号之前需要等待。是解决进程间协作关系的手段
- 互斥与同步的区别
- 前者是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的
- 后者是指在互斥的基础上(大多数情况)通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
临界区管理
临界区调度原则
- 并发进程中与共享变量有关的程序段叫做临界区,共享变量代表的资源叫做临界资源
- 一次至多允许一个进程进入临界区内执行
- 如果已有进程在临界区,其他试图进入的进程应等待
- 如果临界区空闲,则应允许进程进入临界区
- 进入临界区内的进程应在有限时间内退出,以便让等待进程中的一个进入
临界区管理的Peterson算法
bool inside[2];
inside[0] = false;
inside[1] = true;
enum {0,1} turn;
cobegin
process P0() {
inside[0] = true, turn = 1;
while(inside[1] && turn == 1);
{xxx}
inside[0] = false;
}
process P1() {
inside[1] = true, turn = 0;
while(inside[0] && turn == 0);
{xxx}
inside[0] = false;
}
coend
硬件设施
-
关中断:进程在进入临界区之前先关中断,退出临界区时开中断,在关中断期间,进度调度程序失去中断**的机会,不会切换进程,保证了临界区的互斥执行
- 关中断时间过长会影响系统效率,限制处理器交叉执行程序的能力
- 关中断方法不适用于多CPU系统,因为,在一个处理器上关中断,并不能防止进程在其他处理器上执行相同的临界区代码
- 关中断权利赋予用户很危险,如果用户未开中断,系统可能因此终止
-
TS指令
TS(x) { if(x) { x = false; return true; } else { return x; } } bool s = true; cobegin process Pi() { while(!TS(s)); {xxx} s = true; } coend
-
对换指令
bool lock=false; cobegin Process Pi( ) { bool keyi=true; do{ SWAP(keyi,lock); }while(keyi); //上锁 {xxx}; SWAP(keyi,lock);//开锁 } coend
信号量与PV操作
同步与同步机制
- 操作系统实现进程同步的机制称为同步机制,通常由同步原语组成。常用的同步机制有:信号量和PV操作,管程和消息传递
信号量与PV操作
- 信号量实现为记录型数据结构,包含两个分量:一个是信号量的值,另一个是信号量队列的队列指针
- 信号量在进程控制块中。P/wait/up/sleep,V/signal/down/wakeup是原子操作
- 信号量分类
- 按用途:公用信号量(初始值为1,实现互斥,相关进程均可PV);私有信号量(初始值为可用资源数,实现同步,一类进程P,一类进程V)
- 按取值:二元信号量(仅取0或1,解决互斥);一般信号量(初始值为可用资源,实现同步)
- 操作含义
- P操作意味着请求一个资源
- V操作意味着释放一个资源
利用信号量实现互斥
- 方法
- 为临界资源设一互斥信号量mutex,初值为1
- 将临界区置于P(mutex)和V(mutex)之间
- P(mutex)和V(mutex)一定成对出现在同一个进程中
semaphore mutex = 1
cobegin
process Pi() {
P(mutex);
{xxx}
V(mutex);
}
coend
利用信号量实现同步
-
哲学家进餐
- 相邻两个哲学家两两互斥
- 至多允许四位哲学家同时去拿左边的叉子
semaphore fork[5]; semaphore four = 4; for(int i = 0; i < 5; fork[i ++] = 1); cobegin process Pi(int i) { while(1){ think(); P(four); P(fork[i]); P(fork[(i + 1) % 5]); eat(); V(fork[i]); V(fork[(i + 1) % 5]); V(four); } } coend
- 规定奇数号哲学家先拿他左边的叉子,然后再去拿右边的叉子;偶数号哲学家先拿他右边的叉子,然后再去拿左边的叉子
semaphore fork[5]; for(int i = 0; i < 5; fork[i ++] = 1); cobegin process Pi(int i) { while(1) { think(); if(i % 2 == 0) { P(fork[i]); P(fork[(i + 1) % 5]); eat(); V(fork[i]); V(fork[(i + 1) % 5]); } else { P(fork[(i + 1) % 5]); P(fork[i]); eat(); V(fork[(i + 1) % 5]); V(fork[i]); } } } coend
- 仅当哲学家的左右两把叉子均可用时,才允许他拿起叉子进餐,否则一把叉子也不取
semaphore fork[5]; semaphore mutex = 1; for(int i = 0; i < 5; fork[i ++] = 1); cobegin process Pi(int i) { while(1) { think(); P(mutex); P(fork[i]); P(fork[(i + 1) % 5]); V(mutex); eat(); V(fork[i]); V(fork[(i + 1) % 5]); } } coend;
-
生产者消费者
- 指存在数据供给与需求关系的两类进程
item B[k]; // 缓冲池,共有k个缓冲区,临界资源 semaphore empty = k; //空缓冲区个数 semaphore full = 0; // 满缓冲区个数 semaphore mutex = 1; // 保证访问缓冲池的原子性 int in = 0, out = 0; // 读写指针 cobegin process producer() { while(1) { P(empty); P(mutex); increase(B[in]); in = (in + 1) % k; V(mutex); V(full); } } process consumer() { while(1) { P(full); P(mutex); decrease(B[in]); out = (out + 1) % k; V(mutex); V(empty); } }
- mutex互斥,empty和full同步;存在同步关系有:生产者和消费者;存在互斥关系有:生产者和消费者,生产者和生产者,消费者和消费者
-
读者写者
- 若干读者进程和写者进程共享一个数据文件,允许多个Reader进程同时读一个共享对象,但不允许一个Writer进程与其它Reader进程或Writer进程同时访问共享对象
- 读者优先
int reader_count = 0; // 读者数量 semaphore write_block = 1; // 写者互斥 semaphore mutex = 1; // 计算读进程数的互斥 cobegin process reader() { P(mutex); if(reader_count ++ == 1) { P(write_block); // 如果有读者,则锁上写者 } V(mutex); read_file(); P(mutex); if(reader_count -- == 0) { V(write_block); // 说明没有读者了,唤醒写者 } V(mutex); } process writer() { P(write_block); write_file(); V(write_block); } coend // 互斥:单个读者与单个读者,单个写者与单个写者,所有读者与单个写者
- 写者优先
int reader_count = 0; // 读者数量 int writer_count = 0; // 写者数量 semaphore reader_count_mutex = 1; // 计算读进程数的互斥 semaphore writer_count_mutex = 1l // 计算写进程数的互斥 semaphore read = 1; // 读者令牌,使得写者能打断读者进程 semaphore write_block = 1; // 写者互斥 cobegin process reader() { P(read); P(reader_count_mutex); if(reader_count ++ == 1) { P(write_block); } V(reader_count_mutex); V(read); read_file(); P(reader_count_mutex); if(reader_count -- == 0) { V(write_block); } V(reader_count_mutex); } process writer() { P(writer_count_mutex); if(writer_count ++ == 1) { P(read); // 如果有写者,则锁上 } V(writer_count_mutex); P(write_block); write_file(); V(write_block); P(writer_count_mutex); if(writer_count -- == 0) { V(read); // 如果没有写者,则释放 } V(writer_count_mutex); } coend
-
睡眠理发师
int CHAIRS = N; // 椅子数 int waiting = 0; // 等待的顾客 semaphore customers = 0; semaphore barbers = 0; semaphore mutex = 1; cobegin process barber() { while(1) { P(customers); P(mutex); waiting --; V(barbers); V(mutex); cut_hair(); } } process customer() { P(mutex); if(waiting < CHAIRS) { waiting ++; V(customers); V(mutex); P(barbers); get_hair_cut(); } else { V(mutex); } } coend
- 同步:理发师与顾客
- 互斥:理发师与顾客,顾客与顾客
Linux中的同步互斥
管程
概念
- 管程是另一种同步方式,具有封装性
- 管程是由局部于自己的公共变量及其说明和所有访问这些公共变量的过程所组成的软件模块
- 特点
- 共享性:管程可被系统范围内的进程互斥访问,属于共享资源
- 安全性:管程的局部变量只能由管程的过程访问,不允许进程或其它管程直接访问,管程也不能访问非局部于它的变量
- 互斥性:多个进程对管程的访问是互斥的。任一时刻,管程中只能有一个活跃进程
- 封装性:管程内的数据结构是私有的,只能在管程内使用,管程内的过程也只能使用管程内的数据结构
- 结构
- 局部数据和条件变量组成管程内的数据结构
- 过程/函数1~过程/函数k组成管程内的一组过程,对管程内的数据结构进行操作
- 初始化代码对管程内的数据结构进行初始化
- 条件变量与PV信号量的区别
- 条件变量是一种非计数信号量,维护队列时不对其中的等待进程计数。因此在使用条件变量x时,通常需要定义一个与之配套使用的整型变量x-count用于记录条件变量x所维护等待队列中的进程数
- P、V操作中的计数信号量不仅维护相关队列,而且记录队列中进程数
实现
应用
- 哲学家问题
- 生产者消费者问题
进程通信
- 进程同步与互斥的目的是为了保证正确通信
管道通信机制
- 管道是连接读写进程的一个共享文件,允许进程以先进先出(FCFS)方式写入和读出数据,并对读写操作进行同步
- 管道读写进程的同步
- 管道应互斥使用,管道读写不能同时进行。一个进程正在执行管道写入或读出操作时,另一进程须等待。读写结束时,唤醒等待进程,自己则应阻塞
- 读写双方必须能够知道对方是否存在,只有存在才能通信。如果对方已经不存在,就没有必要再发送或接收信息
- 管道空间大小通常是固定的,读写操作时需要考虑写满和读空问题
- 管道类型
- 匿名管道:仅存在于内存中的临时对象,仅用于具有共同祖先进程的父子进程或兄弟进程之间的通信
- 有名管道:又称为FIFO,具有与之关联的文件名、目录项和访问权限,无亲缘关系的进程可以通过引用有名管道的名字进行通信,存在于外存
共享内存通信机制
- 共享内存是允许两个或多个进程共同访问的物理内存区域,是实现进程通信的一种手段
- 共享内存区属于临界资源,读写共享内存区的代码属于临界区
- 操作
- 第一个进程创建共享内存,其它进程则通过创建操作获得共享内存标识符,并据此读写共享内存
- 通信进程将先前创建的共享内存映射到自己的虚拟地址空间,使共享内存成为进程地址空间的一部分
- 通信结束,解除共享内存到通信进程虚地址空间的映射
- 当所有进程不再需要共享内存时删除共享内存
消息传递通信机制
- 消息由消息头和消息体组成
- 组成
- 信箱:是存放信件的存储区域,每个信箱可分成信箱头和信箱体两部分
- 信箱头:指出信箱容量、信件格式、信件位置指针等
- 信箱体:用来存放信件,可分成若干个区,每个区容纳一个信件
- 原语
- send(box,msg):若信箱未满,则把一封信件(消息)发送到信箱A,同时唤醒信件等待者进程,否则发送者阻塞。
- receive(box, msg):若信箱不空,则从信箱A接收一封信件(消息),同时唤醒等待发送者进程;否则接受者阻塞。
Socket通信机制
- Socket通信允许互联的位于不同计算机上的进程之间实现通信功能。唯一一个异机通信
- 分为:IP + 协议 + PORT
- 服务器监听,客户端请求连接,连接确认
信号通信机制
- 信号是一种软中断信号,是对硬件中断机制的软件模拟,利用信号可以实现进程间的通信,一个进程可以向另一进程发送某一信号通知该进程某个异常事件发生
- 产生源:用户,内核,进程
死锁
概念
-
如果在一个进程集合中的每个进程都在等待只能由该集合中的其它进程才能引发的事件,而无限期陷入僵持的局面称为死锁
-
必要条件
- 互斥条件
- 占有和等待条件
- 不剥夺条件
- 循环等待条件
- 第四个条件是前三个条件同时存在时产生的结果,只要破坏这四个条件之一,死锁就可防止
死锁防止
- 破坏互斥:使资源可同时访问而不是互斥使用。该办法对于磁盘适用,对于磁带机、打印机等多数资源不仅不能破坏互斥使用条件,还要加以保证
- 破坏占有和等待:静态分配可以破坏占有和等待条件。静态分配是指一个进程必须在执行前就申请它所需要的全部资源,并且直到它所需要的资源都得到满足后才开始执行。资源利用率低
- 破坏不剥夺条件:即采用剥夺式调度方法。当进程申请资源未获准许时,在等待前主动释放资源。剥夺调度方法目前只适用于内存资源和处理器资源
- 破坏循环等待条件:层次分配策略将资源被分成多个层次,进程按照由低到高的层次顺序申请和得到资源,按照由高到低的层次顺序释放资源。当进程得到某一层的一个资源后,如果需要申请该层的另一个资源,则必须先释放该层中的已占资源
死锁避免
- 死锁避免方法允许系统中同时存在死锁的三个必要条件,即互斥、占有且等待和非抢占;每当进程提出资源申请时,系统分析满足该资源请求时系统是否会发生死锁,若不会发生则实施分配,否则拒绝分配。
- 银行家算法
- 每个客户必须预先说明自己所要求的最大资金量
- 每个客户每次提出部分资金量申请和获得分配
- 如果银行满足了客户对资金的最大需求量,则客户在资金运作后一定可以很快归还资金
- 银行家算法缺点
- 使用银行家算法时,很难在进程运行前知道其所需的资源最大量
- 算法要求系统中的进程必须是无关的,相互间没有同步要求
- 进程的个数和分配的资源数目应该是固定的
检测与解除
- 死锁检测和解除对资源分配不加任何限制,也不采取死锁避免措施,但系统定时运行一个**“死锁检测”程序**,如果检测到系统发生了死锁,再采取措施解除它
- 死锁检测依据的数据结构:进程-资源分配图,描述进程和资源间申请与分配关系的一种有向图,可用以检测系统是否处于死锁状态
- 死锁检测方法:
- 如果进程-资源分配图中无环路,则此时系统没有发生死锁
- 如果进程-资源分配图中有环路,且每个资源类中仅有一个资源,则系统中发生了死锁,此时,环路是系统发生死锁的充要条件,环路中的进程便为死锁进程
- 如果进程-资源分配图中有环路,且涉及的资源类中有多个资源,则环的存在只是产生死锁的必要条件而不是充分条件
- 之后进行简化有向图
- 死锁检测和死锁避免的比较
- 死锁检测算法考虑了检查每个进程还需要的所有资源能否满足要求
- 死锁避免算法则仅根据进程的当前申请资源量来判断系统是否进入了不安全状态
- 在银行家算法中,一次仅允许一个进程提出资源请求,做安全分析并分配资源后,才允许下一个进程提出资源请求
- 死锁检测算法处理的进程-资源图中可以同时存在多个进程的请求边
- 死锁解除的方法
- 立即结束所有进程的执行,并重新启动操作系统。以前工作全部作废,损失可能很大
- 剥夺陷于死锁的进程占用的资源,但并不撤销它,直至死锁解除
- 撤销陷于死锁的所有进程,解除死锁继续运行
- 逐个撤销陷于死锁的进程,回收其资源,直至死锁解除
预测题型
-
哲学家进餐的两种方式
-
读者写者的两种方式
-
死锁的检测与解除
-
红绿灯问题
int RED = 0, GREEN = 1; int light_color; int RED_TIME = N; // 红亮的秒数 int GREEN_TIME = N; // 绿亮的秒数 int wait_num = 0; // 停车的数量 semaphore mutex = 1; semaphore wait = 0; cobegin process light() { while(1) { light_color = RED; while(RED_TIME) { RED_TIME --; } RED_TIME = N; light_color = GREEN; for(int i = 0; i < wait_num; i ++) { V(wait); } while(GREEN) { GREEN_TIME --; } GREEN_TIME =N; } } process car() { while(1) { P(mutex); if(light_color == RED) { wait_num ++; V(mutex); P(wait); P(mutex); wait_num --; V(mutex); } else { V(mutex); } } } coend
思维导图
上一篇: PAT//迪杰特斯拉算法
下一篇: MySQL死锁case分析