Linux信号量详解

信号量(Semaphore)的概念最早由荷兰计算机科学家 Dijkstra(迪杰斯特拉)提出,有时又称“信号灯”。本节,我们将详细地讲解如何使用信号量实现线程同步。

互斥锁类似,信号量本质也是一个全局变量。不同之处在于,互斥锁的值只有 2 个(加锁 "lock" 和解锁 "unlock"),而信号量的值可以根据实际场景的需要自行设置(取值范围为 ≥0)。更重要的是,信号量还支持做“加 1”或者 “减 1”运算,且修改值的过程以“原子操作”的方式实现。

原子操作是指当多个线程试图修改同一个信号量的值时,各线程修改值的过程不会互相干扰。例如信号量的初始值为 1,此时有 2 个线程试图对信号量做“加 1”操作,则信号量的值最终一定是 3,而不会是其它的值。反之若不以“原子操作”方式修改信号量的值,那么最终的计算结果还可能是 2(两个线程同时读取到的值为 1,各自在其基础上加 1,得到的结果即为 2)。

多线程程序中,使用信号量需遵守以下几条规则:

  1. 信号量的值不能小于 0;
  2. 有线程访问资源时,信号量执行“减 1”操作,访问完成后再执行“加 1”操作;
  3. 当信号量的值为 0 时,想访问资源的线程必须等待,直至信号量的值大于 0,等待的线程才能开始访问。

根据初始值的不同,信号量可以细分为 2 类,分别为二进制信号量计数信号量

  • 二进制信号量:指初始值为 1 的信号量,此类信号量只有 1 和 0 两个值,通常用来替代互斥锁实现线程同步;
  • 计数信号量:指初始值大于 1 的信号量,当进程中存在多个线程,但某公共资源允许同时访问的线程数量是有限的(出现了“狼多肉少”的情况),这时就可以用计数信号量来限制同时访问资源的线程数量。

了解什么是信号量之后,接下来教大家如何创建并使用信号量。

信号量的具体用法

POSIX 标准中,信号量用 sem_t 类型的变量表示,该类型定义在<semaphore.h>头文件中。例如,下面代码定义了名为 mySem 的信号量:

#include <semaphore.h>
sem_t mySem;

由此,我们就成功定义了一个 mySem 信号量。但要想使用它,还必须完成初始化操作。

1) 初始化信号量

sem_init() 函数专门用来初始化信号量,语法格式如下:

int sem_init(sem_t *sem, int pshared, unsigned int value);

各个参数的含义分别为:

  • sem:表示要初始化的目标信号量;
  • pshared:表示该信号量是否可以和其他进程共享,pshared 值为 0 时表示不共享,值为 1 时表示共享;
  • value:设置信号量的初始值。

当 sem_init() 成功完成初始化操作时,返回值为 0,否则返回 -1。

2) 操作信号量的函数

对于初始化了的信号量,我们可以借助 <semaphore.h> 头文件提供的一些函数操作它,比如:

int sem_post(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_destroy(sem_t* sem); 

参数 sem 都表示要操作的目标信号量。各个函数的功能如下:

  • sem_post() 函数的功能是:将信号量的值“加 1”,同时唤醒其它等待访问资源的线程;
  • 当信号量的值大于 0 时,sem_wait() 函数会对信号量做“减 1”操作;当信号量的值为 0 时,sem_wait() 函数会阻塞当前线程,直至有线程执行 sem_post() 函数(使信号量的值大于 0),暂停的线程才会继续执行;
  • sem_trywait() 函数的功能和 sem_wait() 函数类似,唯一的不同在于,当信号量的值为 0 时,sem_trywait() 函数并不会阻塞当前线程,而是立即返回 -1;
  • sem_destory() 函数用于手动销毁信号量。

以上函数执行成功时,返回值均为 0 ;如果执行失败,返回值均为 -1。

信号量的实际应用

前面讲过,信号量又细分为二进制信号量和计数信号量,虽然创建和使用它们的方法(函数)是相同的,但应用场景不同。

1) 二进制信号量

二进制信号量常用于代替互斥锁解决线程同步问题,接下来我们使用二进制信号量模拟“4 个售票员卖 10 张票”的过程:

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
//创建信号量
sem_t mySem;
//设置总票数
int ticket_sum = 10;
//模拟买票过程
void *sell_ticket(void *arg) {
    printf("当前线程ID:%u/n", pthread_self());
    int i;
    int flag;
    for (i = 0; i < 10; i++)
    {
        //完成信号量"减 1"操作,否则暂停执行
        flag = sem_wait(&mySem);
        if (flag == 0) {
            if (ticket_sum > 0)
            {
                sleep(1);
                printf("%u 卖第 %d 张票/n", pthread_self(), 10 - ticket_sum + 1);
                ticket_sum--;
            }
            //执行“加1”操作
            sem_post(&mySem);
            sleep(1);
        }
    }
    return 0;
}

int main() {
    int flag;
    int i;
    void *ans;
    //创建 4 个线程
    pthread_t tids[4];
    //初始化信号量
    flag = sem_init(&mySem, 0, 1);
    if (flag != 0) {
        printf("初始化信号量失败/n");
    }
    for (i = 0; i < 4; i++)
    {
        flag = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
        if (flag != 0) {
            printf("线程创建失败!");
            return 0;
        }
    }
    sleep(10);
    for (i = 0; i < 4; i++)
    {
        flag = pthread_join(tids[i], &ans);
        if (flag != 0) {
            printf("tid=%d 等待失败!", tids[i]);
            return 0;
        }
    }
    //执行结束前,销毁信号量
    sem_destroy(&mySem);
    return 0;
}

假设程序编写在 thread.c 文件中,执行过程如下:

[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
当前线程ID:1199965952
当前线程ID:1189476096
当前线程ID:1168496384
当前线程ID:1178986240
1199965952 卖第 1 张票
1189476096 卖第 2 张票
1199965952 卖第 3 张票
1178986240 卖第 4 张票
1168496384 卖第 5 张票
1189476096 卖第 6 张票
1199965952 卖第 7 张票
1178986240 卖第 8 张票
1168496384 卖第 9 张票
1189476096 卖第 10 张票

程序中信号量的初始值为 1,当有多个线程想执行 19~25 行代码时,第一个执行 sem_wait() 函数的线程可以继续执行,同时信号量的值会由 1 变为 0,其它线程只能等待信号量的值由 0 变为 1 后,才能继续执行。

2) 计数信号量

假设某银行只开设了 2 个窗口,但有 5 个人需要办理业务。如果我们使用多线程程序模拟办理业务的过程,可以借助计数信号量实现。

#include <stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<semaphore.h>
//设置办理业务的人数
int num = 5;
//创建信号量
sem_t sem;
//模拟办理业务的过程
void *get_service(void *arg)
{
    int id = *((int*)arg);
    //信号量成功“减 1”后才能继续执行
    if (sem_wait(&sem) == 0)
    {
        printf("---customer%d 正在办理业务/n", id);
        sleep(2);
        printf("---customer%d 已办完业务/n", id);
        //信号量“加 1”
        sem_post(&sem);
    }
    return 0;
}

int main()
{
    int flag,i,j;
    //创建 5 个线程代表 5 个人
    pthread_t customer[5];
    //初始化信号量
    sem_init(&sem, 0, 2);
    for (i = 0; i < num; i++)
    {
        flag = pthread_create(&customer[i], NULL, get_service, &i);
        if (flag != 0)
        {
            printf("线程创建失败!/n");
            return 0;
        }
        else {
            printf("customer%d 来办理业务/n",i);
        }
        sleep(1);
    }

    for (j = 0; j < num; j++)
    {
        flag = pthread_join(customer[j], NULL);
        if (flag != 0) {
            printf("tid=%d 等待失败!", customer[i]);
            return 0;
        }
    }
    sem_destroy(&sem);
    return 0;
}

假设程序编写在 thread.c 文件中,执行过程为:

[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
customer0 来办理业务
—customer0 正在办理业务
customer1 来办理业务
—customer1 正在办理业务
—customer0 已办完业务
customer2 来办理业务
—customer2 正在办理业务
—customer1 已办完业务
customer3 来办理业务
—customer3 正在办理业务
—customer2 已办完业务
customer4 来办理业务
—customer4 正在办理业务
—customer3 已办完业务
—customer4 已办完业务

程序中,sem 信号量的初始化为 2,因此该信号量属于计数信号量。借助 sem 信号量,第 14~21 行的代码段最多只能有 2 个线程同时访问。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/21422.html

(0)
上一篇 2021年7月20日
下一篇 2021年7月20日

相关推荐

发表回复

登录后才能评论