Semaphore
信号量(Semaphore)
线程的信号和进程的信号量类似,使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于 0 的时候,才能访问信号量所代表的公共资源。
sem_t sem_event;
int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化一个信号量
int sem_destroy(sem_t * sem);//销毁信号量
int sem_post(sem_t * sem);//信号量增加1
int sem_wait(sem_t * sem);//信号量减少1
int sem_getvalue(sem_t * sem, int * sval);//获取当前信号量的值
信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是协调共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:
-
测试控制该资源的信号量。
-
若此信号量的值为正,则允许进行使用该资源。进程将信号量减 1。
-
若此信号量为 0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于 0,进程被唤醒,转入步骤(1)。
-
当进程不再使用一个信号量控制的资源时,信号量值加 1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
信号量与普通整型变量的区别:
-
信号量(semaphore)是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap), signal(semap) ; 来进行访问;
-
操作也被成为 PV 原语(P 来源于 Dutch proberen"测试",V 来源于 Dutch verhogen"增加"),而普通整型变量则可以在任何语句块中被访问;
信号量与互斥锁之间的区别主要在于互斥量用于线程的互斥,信号量用于线程的同步;这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
Semaphore 实现
Semaphore 对象里包含一个 count 值和一个队列对象,另外有两个对外 public 的方法,wait()和 signal(),需要特别注意的事,count 值代表的资源数量是不能为负的。为了理解这些属性和方法,我们可以类比一个现实生活中的例子。大家去餐厅吃饭,假设这个餐厅有 10 个座位,有 20 个吃货随机出发去这个餐厅吃饭,那么对应关系是这样的:
- count = 10,10 个座位。
- queue,餐厅位置有限,为了避免混乱,餐厅肯定会吃货们排队。
- wait(),吃货到了餐厅找服务员要位置点餐,这个行为就是 wait。
- signal(),吃货吃完了买单离开位置,这个行为就是 signal。
这其实是一个信号量应用的典型场景,这里关键在于正确理解 wait 和 signal 发生时都有哪些细节步骤。
-
wait():具体到餐厅到例子,20 个人随机出发去餐厅吃饭,有 10 个人先到,然后挨个执行 wait。前 10 个人执行 wait 的时候是有位置的,所以
count>0
,这 10 个人每人都消耗掉一个座位开始吃饭。到第 11 个人到了都时候,count==0
,没有位置了,所以被 suspend,开始加入排队都队列等待。后续所有人都慢慢的到来,但和第 11 个人一样,都只能排队。 -
signal():过了一段时间之后,有个人吃好结账离开了餐厅。这时候如果没有人在排队,位置数量
count++
,没有其它事情发生。但如果有人在排队,比如上面的情况,有 10 个人在等待位置,餐厅会把排在第一个的人安排到刚才空出来的位置,count 值没有变化,但队列的人少了一个。
对于 wait 和 signal 还有两点需要特别注意。也是平时我们使用 Semaphore 时比较容易产生错误的地方。
-
wait 和 signal 都是原子操作。可以简单理解为上面代码里 wait(), signal()两个函数都是加锁的。这个特性其实让 semaphore 的行为变得更简单清晰。大家想象,如果到餐厅的 10 个人是同时到达的,但不是依次询问餐厅是否有位置,而是 10 张嘴同时说话,同时找餐厅要位置,显然情况会变得复杂不好处理。
-
wait 或者 signal 调用的顺序是不确定的。上面的例子中每个人都是随机时间出发,到达餐厅的顺序也是随机的,并不一定先出发的就先到。同理每个人吃饭的时间长短也不一定,有人快有人慢,所以吃好离开餐厅的时间点也是随机的。这里每个人都代表一个线程,因为操作系统线程调度策略导致到底哪个线程先执行也是不确定的。
它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的 PV 操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。所有 信号量 相关的 API 都在名为 include/linux/semaphore.h 的头文件中
我们看到 信号量 机制是有以下的结构体表示的:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
自旋锁 的设计理念是它仅会被持有非常短的时间。但持有自旋锁的时候我们不可以进入睡眠模式因为其他的进程在等待我们。为了防止 死锁 上下文交换 也是不允许的。
当需要长时间持有一个锁的时候 信号量 就是一个很好的解决方案。从另一个方面看,这个机制对于需要短期持有锁的应用并不是最优。为了理解这个问题,我们需要知道什么是 信号量。
就像一般的同步原语,信号量 是基于变量的。这个变量可以变大或者减少,并且这个变量的状态代表了获取锁的能力。注意这个变量的值并不限于 0 和 1。有两种类型的 信号量:二值信号量与普通信号量。
第一种 信号量 的值可以为 1 或者 0。第二种 信号量 的值可以为任何非负数。如果 信号量 的值大于 1 那么它被叫做 计数信号量,并且它允许多于 1 个进程获取它。这种机制允许我们记录现有的资源,而 自旋锁 只允许我们为一个任务上锁。除了所有这些之外,另外一个重要的点是 信号量 允许进入睡眠状态。另外当某进程在等待一个被其他进程获取的锁时,调度器 也许会切换别的进程。
Semaphore:信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
信号量可以分为几类: ² 二进制信号量(binary semaphore):只允许信号量取 0 或 1 值,其同时只能被一个线程获取。
² 整型信号量(integer semaphore):信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为 0。
² 记录型信号量(record semaphore):每个信号量 s 除一个整数值 value(计数)外,还有一个等待队列 List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线程,让其获得信号量,同时信号量再减一。
信号量通过一个计数器控制对共享资源的访问,信号量的值是一个非负整数,所有通过它的线程都会将该整数减一。如果计数器大于 0,则访问被允许,计数器减 1;如果为 0,则访问被禁止,所有试图通过它的线程都将处于等待状态。
计数器计算的结果是允许访问共享资源的通行证。因此,为了访问共享资源,线程必须从信号量得到通行证,如果该信号量的计数大于 0,则此线程获得一个通行证,这将导致信号量的计数递减,否则,此线程将阻塞直到获得一个通行证为止。当此线程不再需要访问共享资源时,它释放该通行证,这导致信号量的计数递增,如果另一个线程等待通行证,则那个线程将在那时获得通行证。
信 号灯与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于”等待”操作,即资源不可用 的话,信号灯机制则侧重于点灯,即告知资源可用;没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持灯亮状 态。当然,这样的操作原语也意味着更多的开销。
信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于 1 的灯数,以表示资源数大于 1,这时可以称之为多元灯。
1. 创建和 注销
POSIX 信号灯标准定义了有名信号灯和无名信号灯两种,但 LinuxThreads 的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。
int sem_init(sem_t *sem, int pshared, unsigned int value) 这 是创建信号灯的 API,其中 value 为信号灯的初值,pshared 表示是否为多进程共享而不仅仅是用于一个进程。LinuxThreads 没有实现多 进程共享信号灯,因此所有非 0 值的 pshared 输入都将使 sem_init()返回-1,且置 errno 为 ENOSYS。初始化好的信号灯由 sem 变量 表征,用于以下点灯、灭灯操作。
int sem_destroy(sem_t * sem) 被注销的信号灯 sem 要求已没有线程在等待该信号灯,否则返回-1,且置 errno 为 EBUSY。除此之外,LinuxThreads 的信号灯 注销函数不做其他动作。
2. 点灯和灭灯
int sem_post(sem_t * sem)
点灯操作将信号灯值原子地加 1,表示增加一个可访问的资源。
int semwait(sem_t * sem) int semtrywait(sem_t * sem)
sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于 0),然后将信号灯原子地减 1,并返回。sem_trywait()为 sem_wait()的非阻塞版,如果信号灯计数大于 0,则原子地减 1 并返回 0,否则立即返回-1,errno 置为 EAGAIN。
3. 获取灯值
int sem*getvalue(sem_t * sem, int _ sval)
读取 sem 中的灯计数,存于*sval 中,并返回 0。
4. 其他
sem_wait()被实现为取消点,而且在支持原子”比较且交换”指令的体系结构上,sem_post()是唯一能用于异步信号处理函数的 POSIX 异步信号 安全的 API。
Semaphore 可以被抽象为五个操作:
-
创建 Create
-
等待 Wait:
线程等待信号量,如果值大于 0,则获得,值减一;如果只等于 0,则一直线程进入睡眠状态,知道信号量值大于 0 或者超时。
-释放 Post
执行释放信号量,则值加一;如果此时有正在等待的线程,则唤醒该线程。
-试图等待 TryWait
如果调用 TryWait,线程并不真正的去获得信号量,还是检查信号量是否能够被获得,如果信号量值大于 0,则 TryWait 返回成功;否则返回失败。
-销毁 Destroy
信号量,是可以用来保护两个或多个关键代码段,这些关键代码段不能并发调用。在进入一个关键代码段之前,线程必须获取一个信号量。如果关键代码段中没有任何线程,那么线程会立即进入该框图中的那个部分。一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量,然后将 Acquire Semaphore VI 以及 Release Semaphore VI 分别放置在每个关键代码段的首末端。确认这些信号量 VI 引用的是初始创建的信号量。
信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的 PV 操作相同。它指出了同时访问共享资源的线程最大 数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用 CreateSemaphore()创建信号量时即 要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会 减 1,只要当前可用资源计数是大于 0 的,就可以发出信号量信号。但是当前可用计数减小到 0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能 在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过 ReleaseSemaphore()函数将当前可用资 源计数加 1。在任何时候当前可用资源计数决不可能大于最大资源计数。信号量是通过计数来对线程访问资源进行控制的,而实际上信号量确实也被称作 Dijkstra 计数器。
PV 操作及信号量的概念都是由荷兰科学家 E.W.Dijkstra 提出的。信号量 S 是一个整数,S 大于等于零时代表可供并发进程使用的资源实体数,但 S 小于零时则表示正在等待使用共享资源的进程数。
P 操作申请资源: (1)S 减 1; (2)若 S 减 1 后仍大于等于零,则进程继续执行; (3)若 S 减 1 后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
V 操作 释放资源: (1)S 加 1; (2)若相加结果大于零,则进程继续执行; (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。
信号量的使用特点使其更适用于对 Socket(套接字)程序中线程的同步。例如,网络上的 HTTP 服务器要对同一时间内访问同一页面的用户数加以限 制,这时可以为没一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用 户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。