《Operating System:Three Easy Pieces》阅读笔记<二十>—信号量

180 阅读4分钟

信号量

我们已经学习了锁和条件变量的含义和使用方法,有人可能发现这两个概念的功能虽然有很大区别,但是形式上有些相似。这是因为锁和条件变量都是信号量(Semaphores) 的高层抽象。所谓信号量就是一个带有数值的对象,是一种专门用于提供不同进程间或线程间同步手段的原语(primitive),由内核来维护,独立于进程。因此可以通过它来进行同步,可以用两个例程去操作它。信号量的初始化非常重要,对应不同情况下信号量的功能。信号量的初始化和两个例程如下图所示。

sem_init()的第二个参数一般都为0,这表示信号量在当前进程中的各个线程之间是共享的。第三个参数表示要将s赋为何值。wait()和post()函数的功能如内部描述的那样。这里有两个注意,一、这两个例程是原子操作;二、s为负数时,它的数值代表正在等待的线程,一般来说这是用户看不到的值。

当信号量被初始化为1时,代表二进制信号量,通过调用信号量的两个例程,这个时候信号量发挥出锁的功能。下图是一个信号量作为锁的功能示例。

我们经常发现一个线程在等待一些事情的发生,另一个线程让一些事情发生,然后发出它已经发生的信号,从而唤醒等待的线程。当信号量被初始化为0时,通过调用信号量的两个例程,这个时候信号量发挥出条件变量的功能。下图是一个信号量作为条件变量的功能示例。

既然信号量还有条件变量的功能,我们同样探究信号量在Producer/Consumer问题上的应用,同样的,这时候要设置两套信号量,一套初始化为0,表示正在等待的consumer线程,另一套初始化为缓冲区的大小,表示当前缓冲区的空位有多少。信号量架构代码和并发操作逻辑如下。

注意如果锁和条件变量构建不当的话可能会有死锁问题。

再看另一个可以利用信号量来解决的问题,Reader-Writer Locks,对于一些数据结构可能需要不同种类的锁。有时甚至需要我们自己去实现一种锁。例如在一个列表的操作中,插入和查询是不能同时进行的,我们用信号量来实现多线程查询和插入操作隔断。具体实现方法如下图代码片段所示。

这种方法有效,但也有一些缺点,特别是在公平方面。例如,读取线程很容易让写入线程starving。因此我们要想办法让写入线程等待时限制读取线程的增加。

还有一些其它问题需要信息量来参与解决。例如哲学家问题,五个哲学家围在餐桌旁,只有五个刀叉,每个哲学家要拿到两个刀叉才能吃饭,明显这里有吃饭时的死锁问题。这个问题虽然没什么实际意义,但是很经典。另一个问题是线程溢出,假如线程非常多,例如几百上千个。就有可能造成系统崩溃,因此我们要设置一个阈值让线程数量维持在一个差不多的位置。这其中的取舍又是很多内容,例如哪些线程是应该留着的?当新线程产生时,是直接让其中断还是取代其它线程?

因为信号量和条件变量、锁的概念是相通的,所以我们可以用条件变量、锁来实现我们自己的信号量,但是基本没人会这么干,因为信号量的优点就是简单通用,一些程序员甚至只使用信号量,避免使用锁和条件变量,因为信号量的简单性和实用性。

小结

线程同步:互斥锁,条件变量,信号量 - Loull - 博客园 (cnblogs.com)

我们学习了锁、条件变量、信号量,有人可能会对这三个类似的概率有些混乱,可以看上面的链接文章,因为内容较多,这里就不再赘述。