一、信号量
- 一个整形 sem(semaphore),两个原子操作;
- P():sem 减1,如果 sem < 0,等待,否则继续;
- V():sem 加1,如果 sem <= 0,唤醒一个等待的 P。
二、信号量的使用
1. 信号量的性质
- 信号量是整数;
- 信号量是被保护的变量,操作必须是原子操作;
- P()能够阻塞,V()不会阻塞;
- 假定信号量是公平的,使用 FIFO。
2. 信号量的分类
- 二进制信号量:可以使 0 或 1
- 一般/计数信号量:可取任何非负值
3. 信号量的应用
- 用信号量实现临界区的互斥访问;
- 用信号量实现条件同步。
4. 生产者-消费者问题
正确性要求
- 在任何一个时间只能有一个线程操作缓冲区(互斥);
- 当缓冲区为空,消费者必须等待生产者(条件同步);
- 当缓冲区满,生产者必须等待消费者(条件同步)。
用信号量描述
- 二进制信号量互斥;
- 一般信号量 fullbuffers;
- 一般信号量 emptybuffers。
信号量操作
Class BoundedBuffer {
mutex = new Semaphore(1);
fullBuffers = new Semaphore(0);
emptyBuffers = new Semaphore(n);
}
BoundedBuffer::Deposit(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();
}
三、信号量的实现
class Semaphore {
int sem;
WaitQueue q;
}
Semaphore::P() {
sem--;
if (sem < 0) {
Add this thread t to q;
block(p);
}
}
Semaphore::V() {
sem++;
if (sem<=0) {
Remove a thread t from q;
wakeup(t);
}
}
- 信号量的双用途:互斥和条件同步,但等待条件是独立的互斥;
- 读、开发代码比较困难;
- 容易出错;
- 不能够处理死锁问题。
四、管程
1. 管程的定义
- 目的:分离互斥和条件同步的关注;
- 管程:一个锁和0或者多个条件变量的组合,锁指的是临界区,条件变量是等待/通知信号量用于管理并发访问共享数据。
- 一般方法:收集在对象/模块中的相关共享数据,定义方法访问共享数据。
2. 管程的组成
锁
- Lock::Acquire() - 等待直到锁可用,然后抢占锁;
- Lock::ARelease() - 释放锁,唤醒等待者,如果有。
条件变量
- 条件变量是管程内的等待机制;
- wait()操作:将自己阻塞在等待队列中,唤醒一个等待者或释放管程的互斥访问;
- signal()操作:将等待队列中的一个线程唤醒,如果队列为空,则等同空操作。
3. 管程的实现
Class Condition {
int numWaiting = 0;
WaitQueue q;
}
Condition::Wait(lock){
numWaiting++;
Add this thread t to q;
release(lock);
schedule(); //need mutex
require(lock);
}
Condition::Signal(){
if (numWaiting > 0) {
Remove a thread t from q;
wakeup(t); //need mutex
numWaiting--;
}
}
4. 用管程解决生产者-消费者问题
classBoundedBuffer {
Lock lock;
int count = 0;
Condition notFull, notEmpty;
}
BoundedBuffer::Deposit(c) {
lock->Acquire();
while (count == n)
notFull.Wait(&lock);
Add c to the buffer;
count++;
notEmpty.Signal();
lock->Release();
}
BoundedBuffer::Remove(c) {
lock->Acquire();
while (count == 0)
notEmpty.Wait(&lock);
Remove c from buffer;
count--;
notFull.Signal();
lock->Release();
}
5. 管程条件变量释放的处理方式
Hansen管程
- 条件变量释放仅是一个提示,当前线程不会放弃管程访问;
- 需要重新检查条件;
- 高效。
Hoare 管程
- 条件变量释放同时表示放弃管程访问;
- 释放后条件变量的状态可用;
- 低效。
6. 基本同步方法
五、读者-写者问题
1. 两种类型的使用者
- 读者:不需要修改数据
- 写者:读取和修改数据
2. 问题的约束
- 允许同一时间有多个读者,但任何时候只有一个写者;
- 当没有写者时,读者才能访问数据;
- 当没有读者和写者时写者才能访问数据;
- 在任何时候只能有一个线程可以操作共享变量。
3. 用信号量解决
共享数据
- 数据集
- 信号量 CountMutex 初始化为1
- 信号量 WriteMutex 初始化为0
- 整数 Rcount 初始化为0
读者优先
- 写者
P(WriteMutex);
write;
V(WriteMutex);
- 读者
P(countMutex);
if (Rcount == 0)
P(WriteMutex);
++Rcount;
V(CountMutex);
read;
P(countMutex);
--Rcount;
if (Rcount == 0)
V(WriteMutex);
V(CountMutex);
4. 用管程解决
状态变量
AR = 0; // # of active readers
AW = 0; // # of active writers
WR = 0; // # of waiting readers
WW = 0; // # of waiting writers
Lock lock;
Condition okToRead;
Condition okToWrite;
读者
Public Database::Read() {
//Wait until no writers;
StartRead();
read database;
//check out – wake up waiting writers;
DoneRead();
}
Private Database::StartRead() {
lock.Acquire();
while ((AW+WW) > 0) {
WR++;
okToRead.wait(&lock);
WR--;
}
AR++;
lock.Release();
}
Private Database::DoneRead() {
lock.Acquire();
AR--;
if (AR ==0 && WW > 0) {
okToWrite.signal();
}
lock.Release();
}
写者
Public Database::Write() {
//Wait until no readers/writers;
StartWrite();
write database;
//check out-wake up waiting readers/writers;
DoneWrite();
}
Private Database::StartWrite() {
lock.Acquire();
while ((AW+AR) > 0) {
WW++;
okToWrite.wait(&lock);
WW--;
}
AW++;
lock.Release();
}
Private Database::DoneWrite() {
lock.Acquire();
AW--;
if (WW > 0) {
okToWrite.signal();
}
else if (WR > 0) {
okToRead.broadcast();
}
lock.Release();
}
六、哲学家就餐问题
1. 问题描述
- 五个哲学家围绕一张圆桌而坐,桌子上放着五支叉子,每两个哲学家之间放一支;
- 哲学家的动作包括思考和进餐,进餐时需要同时拿到左右两边的叉子,思考时将两只叉子放回原处。
2. 哲学家当前的状态
#dedine N 5 // 哲学家的个数
#define LEFT i - 1 % N // 第 i 个哲学家的左邻居
#define RIGHT (i+1)% N // 第 i 个哲学家的右邻居
#define THINKING 0 // 思考状态
#define HUNGRY 1 // 饥饿状态
#define EATING 2 // 进餐状态
int state[N]; // 记录每个人的状态
3. 信号量
semaphore mutex; // 互斥信号量,初值为1,控制对状态的访问
semaphore s[N]; // 同步信号量,初值为0,是否要唤醒邻居
4. philosopher 函数
void philosopher(int i) { // i 的值:0 到 N - 1
while (True){ // 封闭式循环
think(); // 思考中……
take_forks(i); // 拿到两把叉子或被阻塞
eat(); // 吃面条中……
put_forks(i); // 把两把叉子放回原处
}
}
5. take_forks 函数
void take_forks(int i){ // i 的值:0 到 N - 1
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 的值:0 到 N - 1
if (state[i] == HUNGRY &&
state[LEFT] != EATING &&
state[Right] != EATING)
{
state[i] = EATING; // 两把叉子到手
V(s[i]); // 提醒自己可以吃饭了
}
}
7. put_forks 函数
void put_forks(int i){
P(mutex); // 进入临界区
state[i] = THINKING; // 交出两把叉子
test_take_left_right_forks(LEFT) // 看左邻居是否能够进餐
test_take_left_right_forks(RIGHT) // 看右邻居是否能够进餐
V(mutex); // 退出临界区
}