回顾一下:
并发问题:多线程并发导致资源竞争
同步概念:
---------1. 协调多线程对共享数据的访问
---------2.任何时刻只能由一个线程执行临界区代码
确保同步正确的方法
---------底层硬件支持
---------高层次的编程抽象(锁)
信号量是锁机制在同一层上的高层抽象编程方法
一、 信号量semaphore
信号量是操作系统提供的一种协调共享资源访问的方法,用信号量表示系统资源的数量
- 信号是一种抽象数据类型,由一个整型(sem)变量和两个原子操作组成:
1)P():sem减1,如sem<0,表示申请资源(减1)后没有资源了,需要等待
2)V():sem加1,如sem<=0,即一个资源用完(加1)但还是小于0表示还有资源在等待,那么就唤醒一个等待进程 - 信号量是被保护的整数变量。初始化完成后就只能通过P()和V()操作来修改,并且由操作系统来保证PV操作时原子操作
- P可能由于没有资源而进入阻塞状态,但是V操作不会被阻塞
- 假定信号量实现的同步是公平的,线程不会被阻塞在P()操作中,并且信号量等待按先进先出
信号量的实现
class Semophore{
int sem;
WaitQueue q;
}
Semophore::P()
{
sem--;//申请一个资源
if(sem<0)//资源不够,要等待
{
Add this thread t to q;
block(p);
}
Semophore::V()
{
sem++;//释放一个资源
if(sem <=0)//表明有进程在等待,唤醒等待进程
{
Removea thread t form queue q;
wakeup(t);
}
}
P() 和 V()的原子性由操作系统来保护的
信号量的使用
-
信号量可分为两种信号量——
① 二进制信号量:资源数目为0或1
② 资源信号量:资源数目为任何非负值
这两种信号量等价,基于一个可以实现另一个 -
信号量的使用——
① 互斥访问:临界区的互斥访问控制
② 条件同步(调度约束):线程间的事件等待
用信号量实现互斥访问
每类资源设置一个信号量,其初值为1
mutex = new Semaphore(1)//1
mutex ->P();//占用资源
Critical section;//进入临界区
mutex->V();//释放资源
信号量初始值设置为1,占用减1,当其他线程也想执行(进入临界区)就发现信号量已经非正,就必须得等待一个线程执行完释放资源
用信号量实现同步
用示意图来理解同步的问题,同步问题也是调度约束问题,下图可以理解问线程A需要的资源需等待线程B来释放(准备)
条件同步设置一个信号量,其除值为0,当线程A开始运行时,调用了P,减1,资源数非正,就必须得等待,B执行,运行完V,加1,释放资源,线程A才能继续执行。就这样实现了同步
信号量解决“生产者-消费者问题”
- 问题:
· 一个或多个生产者产生数据并将数据放入缓冲区
· 单个消费者每次从缓冲区取出数据
· 在任何一个时刻只有一个生产者或消费者可以访问缓冲区
-
问题分析:
· 任何时刻只能由一个线程操作缓冲区(互斥访问)
· 缓冲区空时,消费者必须等待生产者(条件同步)
· 缓冲区满时,生产者必须等待消费者(条件同步) -
问题解决策略(用信号量描述每个约束:
· 二进制信号量mutex(保证互斥)
· 资源信号量fullBuffers(条件同步,约束生产者)
· 资源信号量emptyBuffers(条件同步,约束消费者) -
转换为实际代码
Class BuoudedBuffer
{
mutex = new Semaphore(1)//初始化的值很重要
fullBuffers = new Semaphore(0);//一开始是空的,消费者取不到值
emptyBuffers = new Semaphore(n);//一开始生产者可以往里面放n个数据
}
BoundeBuffer::Deposit(c)//把c放进去
{
emptyBuffers->P();//空位要少一个
mutex->P();//可以放,就进入临界区
Add c to the buffer;
mutex->V();//
fullBuffers->V();//满一个
}
BoundedBuffer::Remove(c)
{
fullBuffers->P();//有没有能让我移的,移走就少一个
mutex->P();//进入临界区必走操作
Remove c from buffer;
mutex->V();
emptyBuffers->V();//这个空位缓存的资源多一个了
}
总结:使用信号量比较困难
- 读/开发代码比较论难
- 容易出错,比如使用的信号量已经被另一个线程占用了或者忘记释放信号量等
- 不能够处理死锁问题
二、管程monitor
why moniter
改进信号量在处理临界区时候的一些麻烦,比如上述的生产者消费者问题中信号量是分散在生产者和消费者两个不同的进程中的,在这种情况下,PV操作的配对是比较困难的,我们试图将这些PV操作集合到一起,就是管程,也是并发程序的编程方法,为了支持这个操作,加了条件变量
how moniter
- 管程是一种用于多线程互斥访问共享资源的程序结构,采用面向对象方法,简化了线程间的同步控制,在任一时刻最多只有一个线程执行管程代码,与临界区的区别是:正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复
- 管程的使用:在对象/模型中,收集相关共享数据,定义方法来共享数据
- 管程的组成:一个锁用于控制管程代码的互斥访问,0个或多个条件变量(图中的x y condition),用于管理共享数据的并发访问,当条件变量为0时,管程其实就是临界区了
- 条件变量是管程内的等待机制:进入管程的线程因资源被占用而进入等待状态,每个条件变量表示一种等待原因,对应一个等待队列,附着两个相应的操作:
1) wait()操作:将自己阻塞在等待队列中;唤醒一个等待者或释放管程的互斥访问
2) signal()操作:将等待队列中的一个线程唤醒,如果等待队列为空,则等同于空操作一开始所有进程在右上角的排队队列中,排队完后仅从wait操作,等到signal操作被唤醒后,执行这个进程的代码
管程的实现:
脚尖变量初始等待值是0——当要等待的时候进入等待序列,然后释放管程的互斥访问权,然后执行调度就可以切换到另外的进程或者线程了,等切换回自己的时候再再次地去请求管程地访问权限;释放时就从等待队列中挪出来再将等待进程数减1就ok
用信号量解决生产者-消费者问题
首先定义一个管程的lock,根据管程定义,只有一个线程才能进入管程,所以再管程头尾要开/解锁;第一个往缓冲区中放置数据:首先就要检查一下缓冲区是否满了还能不能再放了,如果检测到满了,那么就等在一个条件变量上,这边就等在notfull上,直到有空数据了,notfull响应了,才往下执行,即往缓冲区加数据,加上数据了,不空了,那么自然缓冲区的notEmpty也要响应。ok同理第二个Remove的操作,管程前后加锁保证互斥,如果缓冲区没数据,等候在notEmpty变量队列上,直到有数据再取出数据并响应notFull变量
由此可见,管程是将PV操作都集中到一个模块中,从而简化和降低同步机制的实现难度
管程中关于条件变量释放处理方式的两种方法
管程中条件变量到底是内部线程优先执行还是正占用管程处于执行状态的线程更优先
两个方法的区别就在于左边是虽然T1等待的事件已经出现了,但还是继续执行完再给T1,而右边,T1等待的事件一出现立即就放弃管程的互斥访问权,T1马上就开始执行,在T1执行结束后T2再继续执行。在真实OS中还是左边应用更多,更连续,少一次进程切换
对应代码;
Hansen实际上和之前一样。之所以用while,是因为上图黄色线程必须等蓝色的完成后才会释放,所以说在队列的线程可能不是一个,都在抢线程;而Hoare方法,他是每signal一次,只有一个等待线程,于是用if。
总结:
基本的硬件操作是高层抽象的基础,采取不同的高层抽象的算法策略,可以实现临界区和管程等不同的并发编程策略。