信号量与管程
背景
并发问题:竞态条件
- 多线程并发导致资源竞争
同步概念
- 多线程共享公共数据的协调执行
- 包括互斥与条件同步
- 互斥:任何时刻只能有一个线程执行临界区代码
确保同步正确的方法
-
底层硬件支持
-
高层次的编程抽象(如,锁)
基本同步方法
信号量
希望临界区能有多个线程和进程执行,如读操作,希望能用更高级的同步互斥手段,通过信号量来实现
信号量是一种抽象数据类型
-
由一个整形 (sem)变量和两个原子操作组成
-
P() :sem减1,如果sem<0,等待,否则继续
-
V() :sem加1,如果sem<=0,说明当前有等着的进程,唤醒挂在信号量上的等待进程,可以是一个,可以是多个
信号量与铁路的类比
- 初始化2个资源控制的信号灯
信号量提出之后迅速被用在早起的OS中,如Unix,现在的OS也大量存在。
信号量的特性
-
信号量是被保护的整数变量:
- 初始化完成后,只能通过P()和V()操作修改
- 由操作系统保证,PV操作是原子操作
-
P() 可能阻塞,V()不会阻塞
-
通常假定信号量是“公平的”
-
线程不会被无限期阻塞在P()操作
-
假定信号量等待按先进先出排队
信号量分类
可分为两种信号量
-
二进制信号量:资源数目为0或1
-
一般/计数信号量:资源数目为任何非负值
两者等价:基于一个可以实现另一个
信号量的使用用在两个方面
-
互斥访问:临界区的互斥访问控制
-
条件同步:调度约束——线程间的事件等待
用信号量实现临界区的互斥访问
每个临界区设置一个信号量,其初值为1
必须成对使用P()操作和V()操作
-
P()操作保证互斥访问临界资源
-
V()操作在使用后释放临界资源
PV操作不能次序错误、重复或遗漏
用信号量实现条件同步
每个条件同步设置一个信号量,其初值为0
P()等待,V()发出信号。
生产者-消费者问题:更复杂的情况
一个线程等待另一个线程处理事情:比如生产东西或者消费东西,互斥(锁机制是不够的),还需要条件同步。
有界缓冲区的生产者-消费者问题描述
-
一个或多个生产者在生成数据后放在一个缓冲区里
-
单个消费者从缓冲区取出数据处理
任何时刻只能有一个生产者或消费者可访问缓冲区
问题分析
-
任何时刻只能有一个线程操作缓冲区(互斥访问)
-
缓冲区空时,消费者必须等待生产者(调度/条件同步)
缓冲区满时,生产者必须等待消费者(调度/条件同步)
用信号量描述每个约束
-
二进制信号量mutex
-
计数信号量fullBuffers
- 计数信号量emptyBuffers
二进制信号量mutex初值为0,保证互斥。fullBuffers初值为0,表明Buffers初始为0。emptyBuffers初值为Buffers的size.
P、V操作的顺序有影响吗?生成者两个V交换没有问题,但P交换会造成死锁,P操作会产生阻塞,设置不当会造成死锁
信号量的实现
使用硬件原语:禁用中断、原子指令(test and set),类似锁
例如:使用禁用中断
需要一个整型变量sem和等待队列q。block(p),p睡眠。采用先进先出组织队列q.
信号量的双用途
- 互斥和条件同步
- 但是等待条件是独立的互斥
使用信号量的困难:
- 读/开发代码比较困难:程序员需要非常精通信号量
- 容易出错:使用的信号量已经被另一个线程占用,忘记释放信号量
- 不能够处理死锁问题
OS中其实存在大量的信号量的使用。
管程(Moniter)
目的:分离互斥和条件同步的关注
什么是管程:管程是一种用于多线程互斥访问共享资源的程序结构
-
采用面向对象方法,简化了线程间的同步控制
任一时刻最多只有一个线程执行管程代码
正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复
管程的使用
-
在对象/模块中,收集相关共享数据
-
定义访问共享数据的方法
管程的组成
-
一个锁:控制管程代码的互斥访问
-
0或者多个条件变量:管理共享数据的并发访问
条件变量(Condition Variable)
条件变量是管程内的等待机制
-
线程因资源被占用而进入等待状态
-
每个条件变量表示一种等待原因,对应一个等待队列
Wait()操作
-
释放锁,睡眠,重新获得锁返回
Signal()操作
- 唤醒等待着(或者所有等待着),如果有。如果等待队列为空,则等同空操作(和信号量不一样)
条件变量实现
注意:numWaiting和信号量中的sem不一样,signal的numWaiting不一定会做减1操作。schedule选择下一个线程执行,自身睡眠。wakeup()将当前睡眠状态的线程重新置为ready状态,被调度处于运行时继续往下走。
用管程解决生产者-消费者问题
count记录Buffer的空闲状况,为0表示空,为n表示满。初始时为0。这里的互斥在头和尾,由管程的定义决定。
管程条件变量的释放处理方式
发出signal后的不同处理方式
对于方案一并没有马上让等待的线程被唤醒执行,有可能存在多个等待条件变量队列上的线程被唤醒,大家可能去抢占CPU执行,有可能被唤醒的线程被执行时count已经不为n。而方案二中被唤醒的进程马上执行。
基本同步方法
开发/调试并行程序很难:非确定性的交叉指令
同步结果:
- 锁:互斥
- 条件变量:有条件的同步
- 其他原语:信号量
- 怎样有效的使用这些结果?制定并遵循严格的程序设计、策略
经典同步问题
读者写者问题
动机:共享数据的访问
两种类型的使用者:
-
读者:只读取数据,不修改
-
写者:读取和修改数据
问题约束:
-
同一时刻,允许有多个读者同时读 读读允许
-
没有写者时读者才能读 写读互斥
- 没有读者时写者才能写 读写互斥
- 没有其他写者时写者才能写 写写互斥
用信号量描述每个约束
-
信号量WriteMutex,控制写操作的互斥,初始化为1
-
读者计数Rcount,正在进行读操作的读者数目,初始化为0
信号量CountMutex,控制对读者计数的互斥修改,初始化为1,不加保护的话加或者减会出错。
读者优先策略
-
只要有读者正在读状态,后来的读者都能直接进入
-
如读者持续不断进入,则写者就处于饥饿
写者优先策略
-
只要有写者就绪,写者应尽快执行写操作
-
如写者持续不断就绪,则读者就处于饥饿
用管程实现写者优先
okToWrite.signal()只唤醒一个等待着的WW,但okToRead.broadcast()将所有等待着的WR唤醒。因为只允许一个写者,但读者可以有很多。
哲学家就餐问题
问题描述:
共享数据:
- Bowl of rice (data set)
- Semphore fork[5] initializedto 1
take fork(i) :P(fork[i]) put fork(i):V(fork[i])
方案1:
不正确,可能导致死锁,5个人都拿到了自己左边的叉子。
方案2
互斥访问正确,但每次只允许一人进餐
方案3
没有死锁,可有多人同时就餐
方案4:信号量解决
哲学家要么不拿,要么就拿两把叉子。那么哲学家就有三种状态:思考状态不用叉子、饥饿状态在等待左右叉子;吃饭状态正在使用叉子。
1.必须有一个数据结构,描述每个哲学家当前的状态
#define N 5 //哲学家个数
#define LEFT (i) //第i个哲学家的左邻居
#define RIGHT (i+1)/N //第i个哲学家的右邻居
#define EATTING 2 //进餐状态
#define HUNGRY 1 //饥饿状态
#define THINKING 0 //思考状态
int state[N]; //记录每个人的状态
2.该状态是一个临界资源,对它的访问应该互斥的进行
semaphore mutex; //互斥信号量初始值为1
3.一个哲学家吃饱后,可能要唤醒邻居,存在同步关系
semaphore s[N]; //同步信号量 ,初值0
4.函数philosopher
void philosopher(i)
{
think(i);
take_forks(i); //吃饭前先等待两只叉子
eat();
put_forks(i); //放下叉子,查看左右邻居是否两只叉子都空闲,如果空闲提醒邻居拿起叉子
}
5.函数take_forks的定义:功能是要么拿到两把叉子,妖魔被阻塞起来。
void take_forks(i)
{
P(mutex)
state[i] = HUNGRY; //代表当前哲学家正在等待叉子
test_take_left_right_forks(i); //尝试是否能拿到叉子
V(mutex);
P(s[i]); //如果拿不到叉子就阻塞
}
6.函数test_take_left_right_forks
void test_take_left_right_forks(i)
{
if(state[i] == HUNGRY && state[LEFT] != EATTING && state[RIGTH] != EATTING)
{
state[i] = EATTING; //用EATTING代表当前哲学家能拿到两只叉子
V(s[i]); //如果能够拿到两只叉子,唤醒当前线程
}
}
7.函数put_forks的定义
void put_forks(i)
{
P(mutex)
state[i] = THINKING; //代表当前不需要叉子
test_take_left_right_forks(LEFT);
test_take_left_right_forks(RIGHT);
V(mutex);
}
8.eat应该拿到两把叉子,是临界区,不需要保护,但是think需要保护
void think(i)
{
P(mutex);
state[i] = THINKING;
V(mutex);
}
方案4:信号量解决:管程解决
#define N 5
#define LEFT (i)
#define RIGHT (i+1)/N
#define EATTING 2
#define HUNGRY 1
#define THINKING 0
int state[N];
lock mutex;
Condition c[N];
void philosopher(i)
{
think(i);
take_forks(i);
eat();
put_forks(i);
}
void take_forks(i)
{
lock.acqure();
state[i] = HUNGRY; //代表当前哲学家正在等待筷子,处于阻塞状态
test_take_left_right_forks(i); //尝试是否能拿到叉子
while(state[i] != EATTING)
c[i].wait(&lock);
lock.release();
}
void test_take_left_right_forks(i)
{
if(state[i] == HUNGRY && state[LEFT] != EATTING && state[RIGTH] != EATTING)
{
state[i] = EATTING; //用EATTING代表当前哲学家能拿且会用叉子
condition[i].signal();
}
}
void put_forks(i)
{
lock.acquier();
state[i] = THINKING; //代表当前不需要筷子
test_take_left_right_forks(LEFT);
test_take_left_right_forks(RIGHT);
lock.release();
}
void think(i)
{
lock.acquier();
state[i] = THINKING;
lock.release();
}