欢迎来到经典白学环节!
概念
信号量 Semaphores 是 E.W.Dijkstra 在1965年提出的一种方法。(没错, 就是那个最短路径算法的Dijkstra,银行家算法的Dijkstra,“goto必须死”的Dijkstra)
一个信号量有一个整数count和两个操作”P”和”V”组成:
- “P”操作检查信号量的
count是否大于0,若大于0,这将count减1,并继续; 若count为0,则进程睡眠,而且此时”P”操作仍未完成,待该进程被”V”操作唤醒,“P”才算完成 - “V”操作将
count加1,如果一个或多个进程在该信号量上睡眠,则唤醒其中一个 - “P”和”V”操作都是原子的,不可分割,故也称为”PV”原语
以上描述对线程也是适用的,下面我们也在线程语境下讨论。
P和V是荷兰语Proberen(测试)和Verhogen(增加)的首字母,看着比较眼生, 方便起见, 我们定义信号量Semaphore的接口时用count_down_and_wait表示”P”,increase_and_notify表示”V”。
特别的,limit等于1的信号量保证了只有一个线程能进入临界区,这种信号量被称为binary semaphore,跟mutex是等价的,我们可以用semaphore定义出mutex来。mutex基于的pthread要求加锁和解锁是同一个线程(解铃还须系铃人), 而信号量没有这个要求。
应用场景
假设你有一群生产者线程,一群消费者线程。 一个大小为N的缓冲区,生产者将生产的产品放入缓冲区,消费者则从缓冲区取走产品。 自然我们无法从空的缓冲区取走产品,也无法向满的缓冲区放入产品,所以我们得想一个同步方法。经典地,我们可以用信号量来解决生产者-消费者问题。
问题
mutex + condition_variable与semaphore有同等表达能力,甚至能相互实现,完全可以替代对方,但是semaphore同时具备了互斥和同步的作用,更难使用,更容易出错,所以人们不建议使用了。
- Why has class semaphore disappeared?
Semaphore was removed as too error prone. The same effect can be achieved with greater safety by the combination of a mutex and a condition variable. Dijkstra (the semaphore’s inventor), Hoare, and Brinch Hansen all depreciated semaphores and advocated more structured alternatives. In a 1969 letter to Brinch Hansen, Wirth said “semaphores … are not suitable for higher level languages.” [Andrews-83] summarizes typical errors as “omitting a P or a V, or accidentally coding a P on one semaphore and a V on on another”, forgetting to include all references to shared objects in critical sections, and confusion caused by using the same primitive for “both condition synchronization and mutual exclusion”.
其实什么PV调错对象啦,P了忘记V啦,共享数据的访问代码忘记放在临界区啦,都是小问题,这里参考的文献是Gregory R. Andrews 和 Fred B. Schneider 的 Concepts and Notations for Concurrent Programming( ACM Computing Surveys, Vol. 15, No. 1, March, 1983)是1983年的,那会C++还不知道在哪呢?更别说RAII这个idioms。但是因为semaphore同时具备了互斥和条件同步的语义,就是说,semaphore同时具有了mutex和condition_variable的功能,这使得人们使用semaphore的时候很难区分某个semaphore是用来互斥的,还是用来同步的。
而大部分情况下,semaphore都是用来互斥的,而一个binary semaphore可以进行另一个线程加锁,在另一个线程解锁的行为,很容易导致错误。 mutex则规定了在哪个线程加锁,就得在哪个线程解锁,否则未定义行为,用错就挂,至少容易发现错误。 这使得linux kernel也大范围弃用semaphore。
而条件同步,完成通知等唤醒操作,则有condition_variable等组件可以提供,这样写出来的代码是比较简单的,但如果用semaphore来做条件同步,你看看上面我们实现condition_variable的种种问题,想写对真是不容易。
所以弃用semaphore也不无道理,而且用mutex+condition_variable很容易实现一个semaphore,反过来却困难得多。