一. 信号量
1.1 什么是信号量
1、信号量(Semaphore): 是 Linux 系统中用于进程/线程间同步与互斥 的一种机制。它本质上是一个受保护的整数计数器 ,配合两个原子操作(P 操作和 V操作),用来控制多个进程/线程对共享资源的访问。
2、 线程同步: 让多个线程执行某任务时,具有先后执行顺序
可以将信号量当作一个资源数,若资源数>0,则可以申请成功,申请成功后,该资源数减1,
当资源数为0时,则阻塞等待其>0
3、P操作与V操作:

|-------------|-----------------|--------|---------------------------------------|
| 操作 | 别称 | 作用 | 行为(都是原子操作) |
| P(proberen) | wait / sem_wait | 请求资源 | 如果S>0,使S-1,继续执行 如果S=0,阻塞线程/进程,直到S>0 |
| V(verhogen) | sem_post | 释放资源 | S+1 如果有其他进程/线程正在等待此信号量,则唤醒其中一个 |
**信号量使用步骤:**创建信号量;初始化信号量;申请信号量;释放信号量;销毁信号量。
1.2 为什么使用信号量
1、信号量是进化版的互斥锁(1 -> N)
2、由于互斥锁的粒度 比较大,如果希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。

3、信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
1.3 信号量与互斥量的区别
1、相同:
二值信号量 相当于互斥量
2、不同:
1、**所有者:**互斥量有"所有权"的概念,互斥量总是在同一线程进行加锁与解锁
信号量无"所有权"的概念,可以由一个线程进行**_post** ,另一个线程进行**_wait**
2、**作用不同:**互斥量用于互斥,保护临界区,而信号量则用于同步。
互斥: 指某一时刻只允许一个线程对共享资源进行访问,但是无法限制访问者对资源的访问顺序,即访问是无序的,无法完成同步
**同步:**在互斥的基础上,通过其他机制,可以实现访问者对资源的有序访问。大多情况下,同步已经实现了互斥,在大多数写共享资源下,必定是互斥的,在读共享资源时,可以允许多个访问者同时访问。
3、互斥量只能为0/1,而信号量可以是非负整数
1.4 信号量与条件变量的区别
1、条件变量的等待函数要求传入互斥量作为参数,故条件变量必须配合互斥量同时使用
信号量没有类似的要求。
2、条件变量要求等待与解锁是一个原子操作,如果不是原子操作的话,有可能会出现信号丢失这
个问题
信号量则不要求原子操作,是因为它是计数的,如果判断完条件之后,调用sem_wait之前,信
号量发布(加一)了,那么调用sem_wait时直接不需要等待,直接继续执行,不存在唤醒丢失的
情况。
总之,条件变量唤醒时如果没有线程等待在该条件变量上,那么信号将丢失;而信号量有计数
值,每次信号量post操作都会被记录。
二. 信号量相关函数
1、以下函数都是成功返回0,失败返回-1,并且设置errno(注意这些函数没有pthread_前缀,
表明其可以应用在线程间同步也可以应用于进程间同步)
2、sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使
用文件描述符)。
sem_t sem:规定信号量sem不能 < 0。头文件 <semaphore.h>
|---------------|------------|
| 函数 | 作用 |
| sem_init | 初始化信号量 |
| sem_destroy | 销毁信号量 |
| sem_wait | 信号量-- |
| sem_trywait | 尝试给信号量-- |
| sem_timedwait | 限时尝试给信号量-- |
| sem_post | 信号量++ |
2.1 sem_init

作用与参数:
1、sem_init() 函数会根据参数 sem 所指向的地址对无名信号量进行初始化。值参数则用于指定该信号量的初始值。
2、 pshared 参数用于指示此信号量是用于进程内的线程之间共享 ,还是用于进程之间的共享 。如果 pshared 的值为 0,则该信号量在进程的各个线程之间是共享的,并且应当位于所有线程都能访问的某个地址处(例如,一个全局变量,或者在堆上动态分配的变量)。
如果 pshared 不为零(非0),则该信号量将被多个进程共享,并且应位于共享内存区域中(由于通过 fork(2) 创建的子进程会继承其父进程的内存映射,因此它也可以访问该信号量。)
3、对已初始化的信号量进行重新初始化会导致未定义行为。
value的作用:
1、初值决定了初始时刻最多有多少个线程可同时访问信号量而不阻塞
2、也决定了同一线程可以访问信号量的次数(不阻塞)
**返回值:**成功返回0 失败返回-1并且设置errno
2.2 sem_destroy

作用:
1、sem_destroy() 函数会销毁位于由 sem 指向的地址处的未命名信号量。
2、只有通过 sem_init() 函数初始化的信号量才应使用 sem_destroy() 函数进行销毁。
3、破坏其他进程或线程当前正在等待的信号量会导致未定义的行为。
4、使用已被销毁的信号量会产生成败不确定的结果,除非先使用"sem_init()"函数重新初始化该信号量。
**返回值:**成功返回0 失败返回-1并且设置errno
2.3 sem_wait

作用:
1、sem_wait() 函数会减少指向该参数所指的信号量的值。如果该信号量的值大于零,那么减法操作就会继续进行,并且函数会立即返回。如果该信号量当前的值为零,那么该调用就会被阻塞,直到能够执行减法操作(即信号量的值上升至大于零)或者信号处理程序中断该调用为止。
**返回值:**成功返回0 失败返回-1并且设置errno
2.4 sem_trywait

作用:
1、非阻塞 ,sem_trywait() 的功能与 sem_wait() 相同,不同之处在于:如果减法操作无法立即执行,那么该函数会返回一个错误(将 errno 设置为 EAGAIN),而不是进行阻塞操作。
**返回值:**成功返回0 失败返回-1并且设置errno
2.5 sem_timedwait

作用:
1、sem_timedwait函数 与 sem_wait函数 的功能相同,不同之处在于 abs_timeout 参数用于设定如果减法操作无法立即执行时,该调用应阻塞的时间上限 。abs_timeout 参数指向一个结构体,该结构体指定了自 1970 年 1 月 1 日 00:00:00 +0000(UTC)起的绝对超时时间(以秒和纳秒为单位)。这结构定义如下:
cpp
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
2、如果在调用时超时时间已经过去,而此时无法立即获取信号量,则 sem_timedwait() 函数将返回失败,并返回超时错误(错误码设置为 ETIMEDOUT)。
3、如果操作能够立即执行完成,那么无论 abs_timeout 的值如何,sem_timedwait() 函数都不会因超时错误而失败。此外,在这种情况下,不会对 abs_timeout 的有效性进行检查。
**返回值:**成功返回0 失败返回-1并且设置errno
2.6 sem_post

作用:
1、sem_post函数会增加 (解锁)指向该参数所指的信号量的值。如果信号量的值随后变为大于零,则在sem_wait调用中被阻塞的另一个进程或线程将会被唤醒,并继续去锁定该信号量。
**返回值:**成功返回0 失败返回-1并且设置errno
2.7 示例代码
模拟银行取钱程序
cpp
sem_t sem;
void *task(void *arg)
{
long i = (long)arg;
sem_wait(&sem);//申请成功或者阻塞等待
printf("--[%ld][%ld]:drawing money:%d\n",i,pthread_self(),rand()%10000+1);
sleep(rand() % 3 + 1);
sem_post(&sem);//解锁当前信号量
return NULL;
}
int main()
{
srand(time(NULL));
pthread_t tid[10];
long i = 0;
int ret;
sem_init(&sem, 0, 3);//初始化信号量,初始资源为3
for(i = 0; i < 10; i++){
ret = pthread_create(&tid[i],NULL, task, (void*)i);
if(ret != 0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
}
for(i = 0; i < 10; i++){
ret = pthread_join(tid[i], NULL);
if(ret != 0){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
}
printf("===main:draw finish!\n");
sem_destroy(&sem);//销毁信号量
return 0;
}
三. 生产者消费者信号量模型
图示:

代码实现:
cpp
//利用信号量实现的生产者消费者模型
#define QUEUE_SIZE 5
void sys_err2(int ret, const char *str)
{
fprintf(stderr,"%s error:%s",str,strerror(ret));
exit(1);
}
void sys_err1(const char *str)
{
perror(str);
exit(1);
}
sem_t producter_number, blank_number;//全局信号量:产品数与空格数
int queue[QUEUE_SIZE];//全局数组实现环形队列
void *fpro(void *arg)
{
int i = 0;
while(1){
sem_wait(&blank_number);//没有空格则阻塞,有则--
queue[i] = rand() % 20 + 1;
printf("producter:---%d\n",queue[i]);
sem_post(&producter_number);//产品++且唤醒阻塞在产品上的线程
i = (i + 1) % QUEUE_SIZE;//借助下标实现环形队列
sleep(rand() % 2);
}
}
void *fcom(void *arg)
{
int i = 0;
while(1){
sem_wait(&producter_number);//没有产品则阻塞,有则--
printf("---consumer---%d\n",queue[i]);
queue[i] = 0;
sem_post(&blank_number);//空格++且唤醒阻塞在空格上的线程
i = (i + 1) % QUEUE_SIZE;
sleep(rand() % 4);
}
}
int main()
{
srand(time(NULL));
pthread_t pro_tid, com_tid;
int ret;
ret = sem_init(&producter_number, 0, 0);//初始化产品信号量
if(ret != 0){
sys_err1("sem_init");
}
ret = sem_init(&blank_number, 0, QUEUE_SIZE);//初始化空格信号量
if(ret != 0){
sys_err1("sem_init");
}
ret = pthread_create(&pro_tid, NULL, fpro, NULL);//生产者
if(ret != 0){
sys_err2(ret, "pthread_create pro");
}
ret = pthread_create(&com_tid, NULL, fcom, NULL);//消费者
if(ret != 0){
sys_err2(ret, "pthread_create com");
}
ret = pthread_join(pro_tid, NULL);//
if(ret != 0){
sys_err2(ret, "pthread_join pro");
}
ret = pthread_join(com_tid, NULL);
if(ret != 0){
sys_err2(ret, "pthread_join com");
}
sem_destroy(&producter_number);
sem_destroy(&blank_number);//销毁信号量
return 0;
}
运行结果:
