通过锁可以实现互斥的访问,但是光有互斥不够,还需要同步的机制,甚至在临界区需要多个线程进入临界区执行,所以需要更高层的同步互斥语义。
信号量抽象数据类型
一个整型(sem),两个原子操作
P():sem减 1 ,如果sem <0 ,等待,否则继续; V():sem 加1,如果sem<=0,唤醒一个等待的P
信号量的使用:
信号量特点:
信号量是整数;信号量是被保护的变量:初始化完成后,唯一改变一个信号量的值的办法是通过P()和V(),操作必须是原子;
P()能够阻塞,V()不会阻塞;使用FIFO
两种类型信号量:二进制信号量,可以是0或1;一般/计数信号量,可取任何非负值。两者相互表现,给定一个可以实现另一个。
信号量可以用在2个方面:互斥,条件同步(调度约束——一个线程等待另一个线程的事情发生)
用二进制信号量实现的互斥:模拟LOCK操作。完全可以代替Lock
mutex = new Semaphore(1);
mutex->P();
...
Critical Section;
...
mutex->V();
用二进制信号量实现的调度约束:
condition = new Semaphore(0);
线程A:
...
condition->P();
...
线程B:
...
condition->V();
...
P()等待,V()发出信号。B唤醒A
条件同步的问题,二进制信号量就无法完成了,所以需要计数信号量。
一个线程等待另一个线程处理事情:比如生产东西或消费东西;互斥(锁机制)是不够的。
例:有界缓冲区的 生产者——消费者问题
一个或多个生产者产生数据将数据放在一个缓冲区里;单个消费者每次从缓冲区取出数据;在任何一个时间只有一个生产者或消费者可以访问该缓冲区。
Producer -> Buffer -> Consumer
正确性要求:
在任何一个时间只能有一个线程操作缓冲区(互斥);当缓冲区为空,消费者必须等待生产者(调度/同步约束);当缓冲区满,生产者必须等待消费者(调度/同步约束)
每个约束用一个单独的信号量:
二进制信号量互斥;一般信号量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();
}
P,V操作的顺序有影响吗?
V操作交换顺序没问题。P操作交换顺序有问题,因为P会阻塞,假如BUFFER是满的,先执行Mutex后,emptyBuffers这一步就阻塞,消费者那儿也执行不下去,会导致死锁。
所以要设置好P,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);
}
}
信号量的双用途:互斥和条件同步;但等待条件是独立的互斥。
不能够处理死锁问题。