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();
}