信号量是内核对象,它允许多个线程在同一个时刻访问同一个共享资源,但是需要限制在同一时刻访问此共享资源的最大线程数量。在创建信号量时,要指定允许的最大资源计数和当前可用的资源数。一般将当前可用资源数设置为最大资源数,每增加一个线程对共享资源的访问,当前可用资源数减1。当可用资源计数减小到0时,则说明当前占用资源的线程数达到了所允许的最大数目,其他线程无法再进入,必须等待(阻塞)。占用资源的线程在处理完成后,会释放占用的资源,此时可用资源计数加1,这时等待的线程队列中,会有其中一个线程被唤醒并获取资源。
信号量有两种类型:二进制信号量和计数信号量。
二进制信号量,只有0和1两种取值。一般用于保护一段代码使其每次只被一个线程执行,这种信号量可代替互斥锁来实现对资源的互斥访问。
计数信号量,可以有更大的取值范围,允许有限数目的线程共同访问某个共享资源。
常用的信号量操作有如下一些函数:
信号量的创建
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem即要被创建并初始化的信号量指针;
value用来指定信号量的初始值,也就是共享资源同时可被访问的最大线程数;
第二个参数pshared需要注意,该参数设置为0时,表明该信号量是在进程内使用,即实现线程之间的同步,pshared需要定义在所有线程都可见的位置,比如定义成全局变量,或者动态分配在堆上的变量。该参数设置为非零值时,用于进程之间的同步,这时信号量需要定义在共享内存区域中,任何可以访问该共享内存区域的进程都可以通过sem_post()和sem_wait()操作该信号量。
本篇我们只讨论进程内的同步。
信号量的控制
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem_post()会使信号量的值递增,如果信号量的值大于0,那么另一个阻塞在sem_wait()上等待该信号的线程就会被唤醒,并获取共享资源;
sem_wait()会使信号量的值递减。如果信号量的值大于0,那么执行递减操作并立即返回;若信号量的当前值为0,那么该调用将会被阻塞,直到信号量的值变为正值,或者被某个信号处理函数中断该调用。
sem_trywait(),与sem_wait()作用类似,不同的是当信号量的值为0而无法执行递减操作时,返回一个错误码(EAGAIN),而不是阻塞宿主线程的执行。
sem_timedwait(),与sem_wait()作用类似,不同的是设置了一个阻塞的时长abs_timeout,当阻塞时间超过该时长时,返回一个ETIMEDOUT错误。
信号量的销毁
#include <semaphore.h>
int sem_destroy(sem_t *sem);
sem_destroy()用来销毁一个未命名的信号量,信号量地址由参数sem指定。只有由sem_init()函数初始化的信号量才能调用sem_destroy()销毁。信号量在销毁前要确保没有其他线程或进程在占用它。一个信号量被销毁后不可再被使用,否则会产生未知结果,被销毁的信号量,可通过sem_init()重新初始化后再被使用。
下面我们通过一个例子来理解信号量的工作机制。
假设一个医院的某科室有三个医生,每个医生每次接诊一个病人,那么该科室最多可接诊的病人数量为3。假如一段时间内,该科室仅提供20个挂号名额,也就是说有20人就诊,那么我们如何使用信号量的机制来模拟该科室的就诊情况呢?
/* 医生和病人的例子:3个医生,20个病人,每次每个医生服务一个病人,也就是说,最多有3个病人可同时就诊 */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
/* 信号量定义为全局变量 */
sem_t g_sem;
/* 定义一定时间段内,接诊的病人总数量 */
const int PATIENT_NUM = 20;
/* 每个线程模拟一个病人的就诊 */
void *patient_service(void *arg)
{
int patient_id = *((int *)arg); //输入参数为病患ID
if(sem_wait(&g_sem) == 0) //病人等待服务资源,即医生空闲
{
printf("Patient %d is seeing the doctor. \n", patient_id);
sleep(2); //每个病人的就诊时间为2秒
sem_post(&g_sem); //当前病人服务完成,释放一个医生资源
}
return NULL;
}
int main()
{
/* 初始化信号量,指定为线程之间的共享,信号量值为3 */
sem_init(&g_sem, 0, 3);
pthread_t patient_thread[PATIENT_NUM]; //每个病人为一个线程
for(int i = 0; i < PATIENT_NUM; i++)
{
int ret;
int patientID = i;
/* 为第i个病人创建服务线程 */
ret = pthread_create(&patient_thread[i], NULL, patient_service, &patientID);
if(ret != 0)
{
printf("pthread_create error! \n");
exit(EXIT_FAILURE);
}
usleep(50);
}
/* 连接已终止的线程 */
for(int j = 0; j < PATIENT_NUM; j++)
{
pthread_join(patient_thread[j], NULL);
}
/* 最后释放信号量 */
sem_destroy(&g_sem);
return 0;
}
执行结果: