1--信号量
1-1--基本知识
信号量可用于实现线程间的互斥与同步,其抽象数据类型包括:一个整型数据(Sem)和两个原子操作(P() 和 V());
P() 操作:将 Sem 减1,如果 Sem < 0,则执行 P() 操作的线程会等待,否则继续执行;
V() 操作:将 Sem 加1,如果 Sem <= 0,则执行 V() 操作的线程会唤醒一个等待的线程;
信号量基本概念:
① 信号量是一个整数,也是一个被保护的变量,只能通过 P() 和 V() 原子操作来改变信号量的值;
② P() 操作会阻塞,V() 操作不会阻塞;
③ 信号量是公平的,V() 操作唤醒线程也是公平的,一般采取 FIFO(先进先出)算法来唤醒等待队列中的线程;
④ 信号量有两种类型:第一是二进制信号量,用0和1表示;第二是计数信号量,可用任何非负值表示;
⑤ 信号量的用途:实现线程间的互斥和条件同步;
// 二进制实现线程间的互斥
mutex = new Semaphore(1); // 初始化为1
mutex->P(); // P操作,减1,判断是等待还是继续执行
...
Critical Section; // 临界区
...
mutex->V(); // V操作,加1,是否唤醒一个等待的线程
// 二进制信号量实现同步调度
condition = new Semaphore(0); // 初始化为0
// 线程A
...
condition->P(); // 由于condition初始值为0,执行P()操作变成负数,线程A处于等待状态
...
// 线程B
...
condition->V(); // 执行V()操作,唤醒处于等待状态的线程A
...
// P操作使线程A等待,直到线程B执行V操作来唤醒调度线程A
1-2--基本实例
使用信号量实现线程间同步互斥的实例:有界缓冲区的生产者和消费者问题
① 一个或多个生产者产生数据并将数据存放在一个缓冲区内;
② 单个消费者每次从缓冲区中取出数据;
③ 在任何一个时间里只有一个生产者或消费者可以访问缓冲区;
上述实例抽象三个出三个要求:
① 在任何一个时间只能有一个线程操作缓冲区(互斥);
② 当缓冲区为空时,消费者必须等待生产者(调度/同步约束);
③ 当缓冲区为满时,生产者必须等待消费者(调度/同步互斥);
Class BoundedBuffer{
mutex = new Semaphore(1); // 互斥,初始为1
fullBuffers = new Semaphore(0); // 同步,初始为0,可理解为buffer当前存量
emptyBuffers = new Semaphore(n); // 同步,初始为n,可理解为buffer剩余容量
}
// 生产者
BoundedBuffer::Deposit(c){
emptyBuffer->P(); // 剩余容量减1,当为负值时,说明buffer已满,阻塞所有生产线程,直到生产线程唤醒
mutex->P(); // 互斥锁,只允许一个线程进行临界区
Add c to the buffer;
mutex->V(); // 退出临界区,解锁
fullBuffer->V(); // 当前存量加1
}
// 消费者
BoundedBuffer::Remove(c){
fullBuffers->P(); // 当前存量假1,当为负值时,说明buffer已空,阻塞所有消费线程,直到生产线程唤醒
mutex->P(); // 互斥锁,只允许一个线程进行临界区
Remove c from buffer;
mutex->V(); //退出临界区,解锁
emptyBuffers->V(); // 剩余容量加1
}
1-3--信号量实现
classSemaphore{
int sem;
WaitQueue q;
}
Semaphore::P(){
sem--;
if(sem < 0){
Add this thread t to q; // 将线程t置于等待队列q
block(p); // 睡眠
}
}
Semaphore::V(){
sem++;
if(sem <= 0){
Remove a thread t from q; // 将线程t从等待队列q中移除
wakeup(t); // 唤醒等待队列q中的线程t
}
}
2--管程
2-1--基本知识
信号量机制存在编写程序困难、容易出错等问题;为了更好地实现同步互斥,引入一个抽象的概念称为管程;
管程可抽象为由一个锁和 0 个或多个条件变量构成的结构,锁用于指定临界区(每次只允许一个线程进入管程管理的区域),条件变量用于控制线程的等待和唤醒;
Lock:
Lock::Acquire():一直等待直到锁可用,然后抢占锁;
Lock::Release():释放锁,唤醒等待的线程;
Condition Variable:
Wait():释放锁,线程睡眠
Signal():唤醒等待的线程
Class Condition{
int numWaiting = 0; // 等待的线程数
WaitQueue Q; // 等待队列
}
Condition::Wait(lock){
numWaiting++; // 等待的线程数加1
Add this thread t to q; // 将线程t置于等待队列中
release(lock); // 释放锁
schedule(); // 选择下一个线程执行
require(lock); // 获取锁
}
// 等待操作先释放锁的原因:
// 管程有很多入口,但每次只能开放其中一个入口,只能让一个进程或线程进入管程管理的区域
// 当线程使用wait()条件变量时,因为自身处在等待状态,这时需要释放管程的使用权,也就是释放管程的入口来让其他线程进入,因此要释放锁
Condition::Signal(){
if(numWaiting > 0){ // 表明当前有线程正在等待
Remove a thread t from q; // 从等待队列中移除一个等待的线程t
wakeup(t); // 唤醒线程t
numWaiting--; // 等待线程数减1
}
}
2-2--基本实例
利用管程实现生产者和消费者的问题:
// 初始化
classBoundedBuffer{
Lock lock; // 锁
int count = 0; // 当前容量
// notFull条件变量管理生产线程
// notEmpty条件变量管理消费线程
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();
}
3--经典同步问题
3-1--读者和写者问题
读者和写者问题面向对共享数据的访问,其包含两种类型的使用者:
① 读者:只读取数据,不需要修改数据;
② 写者:读取和修改数据;
读者和写者问题的约束:
① 允许同一时间有多个读者,但在任何时候只有一个写者;
② 当没有写者时,读者才能访问数据;
③ 当没有读者和写者时,写者才能访问数据;
④ 在任何时候只能有一个线程可以操作共享变量;
3-1-1--使用信号量实现读者优先
初始化信号量:
信号量 CountMutex 初始化为 1;
信号量 WriteMutex 初始化为 1;
整数 Rcount 初始化为 0;
// 对于写者
sem_wait(WriteMutex); // 类似于P操作,将WriteMutex减1,当<0时处于等待状态
write;
Sem_post(WriteMutex); // 类似于V操作,将WriteMutex加1,当<=0时唤醒一个等待的线程
// 对于读者
sem_wait(CountMutex); // 执行P操作,将CountMutex减1,确保临界区只有一个读线程
if(Rcount == 0)
sem_wait(WriteMutex); // 确保读者优先,尽管当前读者为0,也要阻塞写者线程
++Rcount; // 读线程数加1
sem_post(CountMutex); // 执行V操作,释放临界区的使用,允许多个读线程抢占临界区
read;
sem_wait(CountMutex); // 执行P操作,确保以下区域任何时候只有一个读者线程
--Rount;
if(Rcount == 0) // 没有读者线程进入读取数据的临界区,才去释放锁,唤醒写者线程
sem_post(WriteMutex); // 执行V操作
sem_post(CountMutex) // 执行V操作
3-1-2--使用管程实现写者优先
使用管程实现写者优先,初始化以下变量:
① AR = 0; // 当前活跃的读者线程数
② AW = 0; // 当前活跃的写者线程数
③ WR = 0; // 当前等待的读者线程数
④ WW = 0; // 当前等待的写者线程数
⑤ Condition okToRead; // 条件变量
⑥ Condition okToWrite; // 条件变量
⑦ Lock lock; // 锁
// 对于读者
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 reader/writers;
StartWrite();
write database;
//check out -- wake up waiting readers/writes;
Done Write();
}
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();
}