【操作系统学习】(二)同步互斥
2017-12-06 20:48
155 查看
临界区
描述
临界区指进程中访问临界资源的一段需要互斥访问的代码
进入区
检查可否进入临界区的一段代码
如可进入,设置相应“正在访问临界区”的标志
退出区
清除“正在访问临界区”的标志
剩余区
代码中的剩余代码
访问规则
空闲则入没有进程在临界区时,任何进程可进入
忙则等待
如果有进程在临界区时,任何进程都不可进入临界区
有限等待
等待进入临界区的进程不能无限的被动等待
让权等待(可选)
不能进入临界区的进程,应该放弃CPU的使用
同步实现方法
1、禁用硬件中断
没有中断,也没有上下文的切换,没有并发执行硬件将中断处理延迟到中断被启用之后
使用计算机体系结构的指令集提供的方法实现
缺点
禁用中断之后,进程无法被停止可能导致饥饿
整个系统都会为此停下来
临界区可能很长
无法确定响应中断所需的时间
2、软件方法
共享变量
线程之间可以通过一些共有的变量去同步它们的行为PS:以下为代码描述
do{ enter section //进入区 critical section exit section //退出区 reminder section }while(1);
//第一种做法
int turn = 0; turn == i ; //表示允许进入临界区的线程的代号 //线程的代码 do{ while(turn!=i); critical section turn =j; reminder section; }while(1);
缺点
满足“忙则等待”,但不满足“空闲则入”
//第二种做法
int falg[2]; flag[0]=flag[1]=0; flag[i] == 1 ; //表示线程i是否在临界区 //线程的代码 do{ while(flag[j]==1); flag[i]=1; critical section flag[i]=0; reminder section; }while(1);
缺点
满足“空闲则入”,不满足“忙则等待”
//第三种做法
int falg[2]; flag[0]=flag[1]=0; flag[i] == 1 ; //表示线程i想要进入在临界区 //线程的代码 do{ flag[i] = 1 ; while(flag[j] == 1); critical section flag[i] = 0; reminder section; }while(1);
缺点
满足“忙则等待”,但不满足“空闲可入”
//Peterson算法
int turn;//表示该谁进入临界区 boolean flag[];//表示进程是否准备好进入临界区 do{ //进入区代码 flag[i] = true; turn = j; while(falg[j] && turn == j); criticla section //退出区代码 flag[i] = false; remainder section }while(1);
//Dekkers算法
int turn;//表示该谁进入临界区 boolean flag[];//表示进程是否准备好进入临界区 do{ //进入区代码 flag[i] = true; while(flag[j] == true ){ if(turn != i ){ flag[i] = false; while(tuen != i){} flag[i] = true; } } criticla section //退出区代码 flag[i] = false; turn = j; remainder section }while(1);
3、基于软件的解决方法的分析
复杂需要两个进程间的共享数据项
需要程序编写者极高的素养
需要忙等待
浪费CPU时间
更高级的抽象方法
描述
硬件提供了一些同步原语中断禁用,原子操作指令等
操作系统提供了更高级的编程抽象来简化进程同步
例如:锁、信号量
用硬件原语来构建
锁(lock)
锁是一种抽象的数据结构一个二进制变量(锁定/解锁)
Lock::Acquire
锁被释放前一直等待,然后得到锁
Lock::Release
释放锁,唤醒任何等待的线程
在进入区得到锁,在退出区释放锁
原子操作指令
现代CPU体系都提供一些特殊的原子操作指令,保证在原子操作时不会被中断测试和置位操作TS(Test-and-Set)(三个操作为一整个不会被中断的原子操作)
从内存中读取值
测试该值是否为1
内存单元值设置为1
交换指令
交换内存中的两个值
使用TS指令实现自旋锁(spinlock)
class Lock{ int value = 0; }; Lock::Acquire() { while(test-and_set(value)) ;//等待 } Lock::Release() { value = 0; }
缺点: 线程在等待的时候也会消耗CPU资源
无忙等待锁(基于上面的自旋锁的忙等待进行优化)
在无法进入临界区的时候,将自己的CPU使用权交出class Lock{ int value = 0; waitQueue q; }; Lock::Acquire() { while(test-and_set(value)){ add this TCB to waitQueue q;//将需要进行CPU使用的进程或线程放入等待队列 schedule(); } } Lock::Release() { value = 0; remove one thread t from waitQueue q;//将等待队列中的线程取出 wakeup(t);//唤醒等待线程队列中的线程 }
优点
适用于单处理器或者共享主存的多处理器中任意数量的进程同步
简单且容易证明
支持多临界区
缺点
忙等待锁会占用CPU时间
可能导致饥饿
进程离开临界区的时候有多个进程等待的情况
可能会出现死锁
如下为一种情况:
拥有临界区的低优先级进程没有获得CPU资源(没法执行完自己的工作)
请求访问临界区的高优先级进程获得处理器资源并等待临界区(如果是执行忙等待)
导致两者都没法执行自己的任务,造成死锁
同步方法总结
锁是一种高级的同步抽象方法互斥可以使用锁来支持
需要硬件的支持才能使用
常用的三种同步实现方法
禁用中断(仅限于单处理器)
软件方法(复杂)
原子指令操作(单处理器、多处理器均可实现)
信号量
简介
信号量是操作系统提供的一种协调软件同步是平等线程中的一种同步协商机制
OS是管理者,地位高于进程
由操作系统来规定谁使用临界区资源
用信号量表示资源的数量
有Dijkstra在60年代提出
早期操作系统的主要同步机制
描述
信号量是一种抽象的数据类型由一个整型变量(sem)和两个原子操作组成
P(Prolaag(荷兰语:尝试减少))
sem减一
如果sem<0,进入等待,否则继续
V(Verhoog(荷兰语:增加))
sem加一
如果sem<=0,唤醒一个处于等待状态的进程
信号量是被保护的整型变量
初始化完成后,只能通过P()和V()操作修改
由操作系统保证,PV操作是原子操作
P()可能被阻塞,V()不会被阻塞
通常假定信号量是“公平的”
线程不会无限期的被阻塞在P()操作
假定信号量等待是按照先进先出排队(自旋锁无法实现先进先出)
信号量的实现
class Semaphore{ int sem; waitQueue q; }; Semaphore::P(){ sem--; if(sem < 0){ add this thread t to q; block(p);//阻塞P } } Semaphore::V(){ sem++; if(sem<=0){ remove a thread t from q; wakeup (t); } }
信号量的分类
可分为两种信号量二进制信号量:资源数量为0或1
资源信号量:资源可为任何非负数
两者等价
基于一个可以实现另一个
信号量的使用
互斥访问
临界区的互斥访问控制
条件同步
线程间的事件等待
用信号量实现临界区的互斥访问
mutex = new Semaphore(1);//设置资源信号量,初始值为1 mutex->P(); Criticla Section; mutex->V();
必须成对使用P()和V()
P()保证互斥访问临界资源
V()保证在使用后释放临界资源
PV 解决生产者消费者问题
生产者 –> 缓冲区 –> 消费者问题分析
任何时刻只能有一个线程操作缓冲区(互斥访问)
缓存区空时,消费者必须等待生产者(条件同步)
缓冲区满时,生产者必须等待消费者(条件同步)
用信号量描述每个约束
二进制信号量 mutex
资源信号量 fullBuffers
资源信号量 emptyBuffers
//信号量 class BoundedBuffer{ mutex = new Semaphore(1); fullBuffers = new Semaphore(0); emptyBuffers = new Semaphore(n);//资源 }; //生产者 BoudedBuffer::Deposit(c){ emptyBuffers->p(); mutex->P(); Add c to the buffer; mutex->V(); fullBuffers->V(); } //消费者 BoudedBuffer::Deposit(c){ fullBuffers->p(); mutex->P(); Remove c from the buffer; mutex->V(); emotyBuffers->V(); }
缺点和困难
读写代码很难程序员需要能运用信号量机制
容易出错
使用的信号量已经被另一个线程占用
忘记释放信号量
不能处理死锁问题
管程
描述
管程是一种用于多线程互斥访问共享资源的程序结构采用面向对象方法,简化了线程间的同步控制
任一时刻最多只有一个线程执行管程代码
正在管程中的线程可临时放弃管程的互斥访问,并等待时间出现时回复
管程的使用
在对象/模块中,收集相关共享数据
定义访问共享数据的方法
组成
一个锁控制管程代码的互斥访问
0或者多个条件变量
管理共享数据的并发访问
条件变量
条件变量是管程内的等待机制进入管程的线程因为资源被占用而进入等待状态
每个条件变量表示一种等待原因,对应一个等待队列
wait()操作
将自己阻塞在等待队列中
唤醒一个等待着管程释放的互斥访问
signal()操作
将等待队列中的一个线程唤醒
如果等待队列为空,则等同空操作
条件变量实现
class Condition{ int numWaiting = 0; WaitQueue q; }; Condition::Wait(lock){ numWaiting++; //等待数加一 add this thread t to q;//放入等待队列 release(lock);//释放管程使用权 schedule();//执行调度 require();//完成调度后,请求管程的访问权 } Condition::Signal(){ if(numWaiting > 0){ remove a thread t from q; wakeup(t); numWaiting--; } }
用管程解决生产者- 消费者问题
代码实现:class BoundedBuffer{ ... Lock lock; int count=0; Condition notFull,notEmpty; }; //生产者 BoudedBuffer::Deposit(c){ lock->Acquire(); while(count -- ){ notFull.Wait(&lock); } Add c to the buffer; count++; notEmpty.Signal(); lock->Release(); } //消费者 BoudedBuffer::Deposit(c){ lock->Acquire(); while(count -- ){ notEmpty.Wait(&lock); } Remove c from the buffer; count -- ; notFull.Signal(); lock->Release(); }
管程条件变量的释放处理方式
根据方式不同可分为:Hansen管程
Hoare管程
相关文章推荐
- 操作系统学习笔记(9) 互斥和同步的信号量算法
- 操作系统学习笔记(10) 互斥和同步的经典问题
- 操作系统学习笔记(8) 互斥和同步的实现算法
- 操作系统学习笔记(13) 互斥与同步的经典问题 -哲学家进餐问题
- 操作系统之同步与互斥
- 【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】全面解析Linux内核的同步与互斥机制--互斥篇
- 操作系统中的互斥,同步与死锁
- 操作系统精髓与设计原理--并发性:互斥和同步
- 内功修炼之操作系统学习(三:同步、通信及死锁)
- 《操作系统》第5章:互斥和同步
- 操作系统经典同步互斥问题——生产者消费者问题
- 操作系统学习-5. 进程同步
- 2011/5/28操作系统学习笔记之经典同步问题 【转】
- 【操作系统】进程(线程)间同步互斥问题——熟睡的理发师问题
- VxWorks操作系统指南(1.4) 通信、同步和互斥机制
- 【操作系统】进程(线程)间同步互斥问题——高校上机实习问题
- 计算机操作系统之四:进程的互斥同步与通信
- 学习笔记 --- LINUX的同步互斥机制 --- 自旋锁与信号量的区别
- 操作系统--进程间的通信,同步和互斥等
- 张孝祥[致敬]-多线程学习第03课-线程的互斥与同步