linux_信号量(详解)

[复制链接]
发表于 2026-10-3 22:36:55 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

信号量在进程和线程编程里出现频率极高:它既是互斥锁,也能当条件变量用。这篇把信号量的本质、P/V 原语的含义、POSIX 那几个接口的返回值,以及互斥和同步两个完整示例一次讲清楚。

一、信号量是什么

信号量本质上是一个非负整数计数器,用来控制对公共资源的访问。编程时通过它的值判断能不能访问:值大于 0 就可以访问,否则调用者阻塞。它的操作叫 PV 操作:

  • P 操作:让信号量减 1。如果减完之后小于或等于 0,调用者阻塞;只有值大于 0 才能真正减 1 返回。
  • V 操作:让信号量加 1。如果有别的进程/线程正在等这个信号量,就把它唤醒继续跑。

这两个操作都是原子的,不用自己加锁担心线程安全。信号量主要用在两种场景:

  • 互斥:几个进程(或线程)之间往往只设一个信号量,谁抢到谁进临界区。
  • 同步:会设多个信号量并给不同的初始值,用初始值控制它们的执行次序。

二、POSIX 信号量接口

2.1 初始化

#include <semaphore.h>

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

功能是创建一个信号量并初始化它的值。参数 sem 是信号量地址;pshared 等于 0 表示在线程间共享,不等于 0 表示在进程间共享;value 是初始值。成功返回 0,失败返回 -1。

2.2 P 操作

int sem_wait(sem_t *sem);      // 阻塞等待获取
int sem_trywait(sem_t *sem);   // 非阻塞,拿不到立刻返回

sem_wait 把信号量减 1,若值小于等于 0 就阻塞调用者;sem_trywait 同样减 1,但值不对时立即返回,不会卡住。均成功返回 0、失败返回 -1。

2.3 V 操作

int sem_post(sem_t *sem);

执行一次 V 操作,信号量加 1;若有等待者被挂起,会发信号把它唤醒。

2.4 查值与销毁

int sem_getvalue(sem_t *sem, int *sval);   // 把当前值存进 sval
int sem_destroy(sem_t *sem);              // 删除信号量

两者都是成功返回 0、失败返回 -1。用完记得 destroy,否则资源不会释放。

三、用信号量做互斥

互斥场景只需要一个信号量,初始值设 1。两个线程都去 P,先抢到的继续执行,后抢到的阻塞;V 之后另一个才能接上。下面用两个线程交替打印字符串来演示(否则两个线程的 putchar 会互相插花):

sem_t sem;                       // 第一步:创建信号量

void printer(char *str)
{
    sem_wait(&sem);              // 第三步:P 操作
    while (*str) {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    sem_post(&sem);              // 第四步:V 操作
}

void *thr_fun1(void *arg) { char *str1 = "change"; printer(str1); }
void *thr_fun2(void *arg) { char *str2 = "word";   printer(str2); }

int main()
{
    pthread_t th1, th2;

    sem_init(&sem, 0, 1);        // 第二步:初始化,值为 1

    pthread_create(&th1, NULL, thr_fun1, NULL);
    pthread_create(&th2, NULL, thr_fun2, NULL);
    pthread_join(th1, NULL);
    pthread_join(th2, NULL);
    printf("
");
    sem_destroy(&sem);           // 第五步:销毁
    return 0;
}

注意顺序是「先 P 再进临界区,出临界区再 V」;如果忘了 V,另一个线程就永远堵着了。

四、用信号量做同步

同步要的是次序:让打印线程等生成线程,一个接一个来。做法是建两个信号量,一个初值 1、一个初值 0,用初值制造先后关系:

sem_t sem_p, sem_g;      // 第一步:创建两个信号量
char ch = 'a';

void *thr_g(void *arg)          // 生成线程
{
    while (1) {
        sem_wait(&sem_g);       // 第四步:P
        ch++;
        sleep(1);
        sem_post(&sem_p);       // 第六步:V
    }
    pthread_exit(0);
}

void *thr_p(void *arg)          // 打印线程
{
    while (1) {
        sem_wait(&sem_p);       // 第三步:P
        printf("%c", ch);
        fflush(stdout);
        sem_post(&sem_g);       // 第五步:V
    }
    pthread_exit(0);
}

int main()
{
    pthread_t th1, th2;

    sem_init(&sem_p, 0, 1);     // 第二步:初始化
    sem_init(&sem_g, 0, 0);

    pthread_create(&th1, NULL, thr_p, NULL);
    pthread_create(&th2, NULL, thr_g, NULL);
    pthread_join(th1, NULL);
    pthread_join(th2, NULL);
    printf("
");
    sem_destroy(&sem_p);
    sem_destroy(&sem_g);
    return 0;
}

sem_p 初值 1 代表「有数据可打印」,sem_g 初值 0 代表「还没生成」。打印方先 P 不到就阻塞,生成方 P 到之后加字符再 V,次序就固定住了。这就是「用初始值安排执行顺序」的实际用法。

五、什么时候用信号量,什么时候用互斥锁

  • 只是想保证同一时刻只有一个线程进临界区:用 pthread_mutex_t 更直接,语义清楚、调试友好。
  • 需要「资源数量」的Semaphore场景(比如线程池还剩几个空闲位置、缓冲区还剩几个空位):信号量天然表达这个含义,用它。
  • 跨进程共享资源:POSIX 信号量支持 pshared != 0,这是互斥锁(pthread_mutex)不好办的地方。

换句话说,单值且高频的互斥优先 mutex,要计数、要跨进程、要当条件配合就用信号量。

六、几个容易漏的坑

  • sem_init 的 pshared 传 0 是线程共享,不等于 0 才是进程共享,传反了跨进程会失效。
  • 在信号处理函数里用 sem_post 是安全的(async-signal-safe),但 sem_wait 不是,信号里别去等它。
  • 线程退出前记得 join,否则信号量状态会残留,另一个线程可能永远等不到 V。
  • free 掉信号量之前先把所有 wait 者撤掉,不然它们醒来面对的是已销毁的对象。

qidao123.com企服评测-应用市场 编辑提示

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表