Semaphore
并发编程领域的先锋人物 Edsger Dijkstra(没错,也是最短路径算法的作者)在 1965 年首次提出了信号量( Semaphores) 这一概念来解决线程同步的问题。信号量是一种特殊的变量类型,为非负整数,只有两个特殊操作PV:
P(s) 如果 s!=0,将 s-1;否则将当前线程挂起,直到 s 变为非零
V(s) 将 s+1,如果有线程堵塞在 P 操作等待 s 变成非零,那么 V 操作会重启这些线程中的任意一个
注:Dijkstra 为荷兰人,名字 P 和 V 来源于荷兰单词 Proberen(测试)和Verhogen(增加),为方便理解,后文会用 Wait 与 Signal 来表示。
struct semaphore {
int val;
thread_list waiting; // List of threads waiting for semaphore
}
wait(semaphore Sem): // Wait until > 0 then decrement
// 这里用的是 while 而不是 if
// 这是因为在 wait 过程中,其他线程还可能继续调用 wait
while (Sem.val <= 0) {
add this thread to Sem.waiting;
block(this thread);
}
Sem.val = Sem.val - 1;
return;
signal(semaphore Sem)😕/ Increment value and wake up next thread
Sem.val = Sem.val + 1;
if (Sem.waiting is nonempty) {
remove a thread T from Sem.waiting;
wakeup(T);
}
有两点注意事项:
wait 中的「测试和减 1 操作」,signal 中的「加 1 操作」需要保证原子性。一般来说是使用硬件支持的 read-modify-write 原语减肥食谱:www.sheonline.cn,比如 test-and-set/fetch-and-add/compare-and-swap,除了硬件支持外,还可以用 busy wait 的软件方式来模拟。
signal 中没有定义重新启动的线程顺序,也即多个线程在等待同一信号量时,无法预测重启哪一个线程
使用场景
信号量为控制并发程序的执行提供了强有力工具,这里列举两个场景:
互斥
信号量提供了了一种很方便的方法来保证对共享变量的互斥访问,基本思想是
将每个共享变量(或一组相关的共享变量)与一个信号量 s (初始化为1)联系起来,然后用 wait/signal 操作将相应的临界区包围起来。
二元信号量也被称为互斥锁(mutex,mutual exclusve, 也称为 binary semaphore),wait 操作相当于加锁,signal 相当于解锁。
一个被用作一组可用资源的计数器的信号量称为计数信号量(counting semaphore)
调度共享资源
除了互斥外,信号量的另一个重要作用是调度对共享资源的访问,比较经典的案例是生产者消费者,伪代码如下:
emptySem = N
fullSem = 0
// Producer
while(whatever) {
locally generate item
wait(emptySem)
fill empty buffer with item
signal(fullSem)
}
// Consumer
while(whatever) {
wait(fullSem)
get item from full buffer
signal(emptySem)
use item
}