马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
信号量在进程和线程编程里出现频率极高:它既是互斥锁,也能当条件变量用。这篇把信号量的本质、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企服评测-应用市场 编辑提示 |