Linux系统编程 -- POSIX无名信号量

信号量

  大家知道,互斥锁可以用于线程间同步,但是,每次只能有一个线程抢到互斥锁,这样限制了程序的并发行。如果我们希望允许多个线程同时访问同一个资源,那么使用互斥锁是没有办法实现的,只能互斥锁会将整个共享资源锁住,只允许一个线程访问。

  这种现象,使得线程依次轮流运行,也就是线程从并行执行变成了串行执行,这样与直接使用单进程无异。

  于是,Linux 系统提出了信号量的概念。这是一种相对比较折中的处理方式,它既能保证线程间同步,数据不混乱,又能提高线程的并发性。注意,这里提到的信号量,与我们所学的信号没有一点关系,就比如 Java 与 JavaScript 没有任何关系一样。

  信号量,是一种相对折中的处理方式,既能保证同步,数据不错乱,又能提高线程并发

信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。

信号量用于互斥:
            image.png

信号量用于同步:
            image.png

信号量的分类

Linux 提供两种信号量:

  1. 内核信号量,由内核控制路径使用

  2. 用户态进程使用的信号量,这种信号量又分为 POSIX 信号量 与 SYSTEMV 信号量,其中 POSIX 信号量又分为有名信号量于无名信号量
    a. 有名信号量:其值保存在文件中,既可以用于线程间同步或互斥,也可以用于进程间同步或互斥
    b. 无名信号量:其值保存在内存中,一般用于线程间同步或互斥
    c. 他们的区别于管道类似

本文主要就用户态中的 POSIX 信号量进行讲解

POSIX

无名信号量

信号量数据类型为: sem_t,本质上是一个结构体,但在应用期间可以简单看做为整数,忽略实现细节。

所需要的头文件为: #include <semaphore.h>
image.png

以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置 errno,于前面讲的有pthread前缀的函数返回方式不同。

初始化信号量

函数原型: int sem_init(sem_t *sem, int pshared, unsigned int value);

作用: 创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。

参数:

  • sem:信号量的地址
  • pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享
  • value:信号量的初始值

销毁信号量

函数原型: int sem_destroy(sem_t *sem);

作用: 删除 sem 标识的信号量。

参数:

  • sem:信号量地址。

信号量 P 操作(减 1)

函数原型: int sem_wait(sem_t *sem);

作用: 将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。

参数:

  • sem:信号量的地址。

补充两个函数
int sem_trywait(sem_t *sem);
非阻塞的方式来对信号量进行减 1 操作。若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
非阻塞的方式来对信号量进行减 1 操作。并设置等待时间,如果时间到了对信号的操作失败,函数立即返回。

函数原型:

作用:

参数:

信号量 V 操作(加 1)

函数原型: int sem_post(sem_t *sem);

作用: 将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。

参数:

  • sem:信号量的地址。

获取信号量的值

函数原型: int sem_getvalue(sem_t *sem, int *sval);

作用: 获取 sem 标识的信号量的值,保存在 sval 中。

参数:

  • sem:信号量地址。
  • sval:传出参数,保存信号量值的地址。

通过信号量实现生产者消费者模型

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#define NUM 5 // 创建最大存储数据量

int que[NUM];  //全局数组实现循环队列
sem_t block_num, data_num; //创建两个信号量

void *produce(void *arg){
    int i = 0;
    while(1){
        //要产生数据,那么能存放数据的个数--,
        //如果数据存满了 block_num == 0,则应该阻塞在这里,等消费者拿走数据后再继续生产
        sem_wait(&block_num);  
        que[i] = rand() % 1000 + 1; //模拟生产数据
        printf("========Produce:%d\n", que[i]);
        i = (i+1) % NUM; //循环队列
    
        sem_post(&data_num); //成功产生一个数据,放入缓冲区,则产品的数量++;

        sleep(rand()%1);
    }   
}

void *consumer(void *arg){
    int i = 0;
    while(1){
        //取出数据,如果此时data_num == 0,则会阻塞,等待生产者生产好数据后再继续执行
        sem_wait(&data_num);
        printf("------Consumer:%d\n", que[i]);
        i = (i+1) % NUM;
    
	//取走了一个数据,则能存放数据的空位++
        sem_post(&block_num);  

        sleep(rand()%3);
    }   
}

int main(){
    srand(time(NULL));
    sem_init(&block_num, 0, NUM);
    sem_init(&data_num, 0, 0); 

    pthread_t tid, cid;
    pthread_create(&tid, NULL, produce, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(tid, NULL);
    pthread_join(cid, NULL);

    sem_destroy(&block_num);
    sem_destroy(&data_num);
    return 0;
}

该代码为突出逻辑,并未检查返回值,是错误的示例。