Linux系统编程-信号量(线程同步机制)

一. 信号量

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;
}

运行结果:

相关推荐
iRayCheung1 小时前
virtualbox安装的ubuntu系统跑numpy报错
linux·ubuntu·numpy
无限进步_1 小时前
Linux进程等待——wait、waitpid与僵尸进程
linux·运维·服务器·开发语言
2401_834636991 小时前
Linux集群技术-高可用与负载均衡实战解析
linux·运维·负载均衡
吠品1 小时前
处理 Python 类继承中那些变来变去的初始化参数
linux·前端·python
帅大大的架构之路2 小时前
linux上面的一些小知识点
linux·运维·服务器
光电笑映2 小时前
进程间通信:深入 System V IPC:共享内存、消息队列与信号量
linux·运维·服务器·c++
RisunJan2 小时前
Linux命令-patch (为开放源代码软件安装补丁程序)
linux·服务器·算法
皆圥忈2 小时前
_Linux文件系统与磁盘结构深度解析
linux
向日葵.2 小时前
linux & qnx & git 命令 2
linux·运维·git