文章目录
一、概述
1.1 基本概念
信号量与信号无关
信号量相当于初始值为 N 的互斥量。N 值,表示可以同时访问共享数据的线程数
-
本质:
信号量(sem_t
)是一个非负整数计数器,用于控制多个线程或进程对公共资源的访问,保证同步和互斥。 -
作用:
- 互斥:一次只允许一个线程或进程访问资源(类似互斥锁)。
- 同步:按一定顺序协调线程或进程的执行(例如生产者–消费者模型)。
-
PV 操作:
- P 操作(
sem_wait
):信号量 -1,如果结果 < 0 则阻塞等待 - V 操作(
sem_post
):信号量 +1,并唤醒等待线程
- P 操作(
-
访问判断:
- 信号量值 > 0:可直接获取资源(执行 P 操作)。
- 信号量值 = 0:资源不可用,请求会阻塞(除非使用非阻塞方式)。
1.2 数据类型
#include <semaphore.h>
sem_t sem; // 信号量变量
- sem_t 是信号量类型
- 值不能小于 0
二、主要 API
这些函数都在
<semaphore.h>
中声明,成功返回0
,失败返回-1
并设置errno
(没有pthread_
前缀)。
编译时指定线程库 -pthread
2.1 初始化与销毁
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem
:信号量变量地址。pshared
:0
:线程间共享(常用)。- 非 0 :进程间共享(必须把
sem
放在可被多个进程映射访问的共享内存(如mmap
)中)。
value
:信号量的初始值,N 值,代表同事访问共享数据的个数。决定并发数;1
类似互斥锁。
int sem_destroy(sem_t *sem);
- 销毁信号量(在不再使用时释放资源)。
2.2 P 操作(获取资源 / 减 1)
int sem_wait(sem_t *sem); // 阻塞方式
int sem_trywait(sem_t *sem); // 非阻塞方式
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 限时阻塞
说明:
sem_wait
:如果信号量为 0,阻塞直到可用。sem_trywait
:立即返回,不阻塞。若不可用返回失败。sem_timedwait
:阻塞到超时时间(绝对时间)为止。- 一次调用做一次
--
操作。当信号量的值为0时,再次--
信号量会阻塞。
时间结构 timespec
:
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
};
示例(定时 1 秒):
time_t cur = time(NULL);
struct timespec t;
t.tv_sec = cur + 1;
t.tv_nsec = 0;
sem_timedwait(&sem, &t);
2.3 V 操作(释放资源 / 加 1)
int sem_post(sem_t *sem);
- 作用:
- 信号量值 +1。
- 如果有线程阻塞在
sem_wait
上,则唤醒一个。 - 一次调用做一次
++
操作。当信号量的值为 N 时,再次++
信号量会阻塞。
2.4 获取当前信号量值
int sem_getvalue(sem_t *sem, int *sval);
- 作用:
- 将当前信号量值存入
*sval
。
- 将当前信号量值存入
- 用途:
- 调试或监控信号量状态。
- 不建议依赖此值做同步判断(存在竞态)。
三、生产者消费者模型
- 有一个固定大小的仓库(这里用
queue[NUM]
的环形队列模拟)。 - 生产者负责往仓库放产品。
- 消费者负责从仓库取产品。
- 规则:
- 仓库满了,生产者必须等(不能生产)。
- 仓库空了,消费者必须等(不能消费)。
问题是:如何让生产者和消费者在多线程环境下安全且高效地协作,而不会出现超产、空取或者数据错乱。
用两个信号量解决同步问题:
blank_num
(空位数)- 初始值 = 仓库容量
NUM
- 生产前
sem_wait(blank_num)
:没空位就阻塞。 - 消费后
sem_post(blank_num)
:释放空位给生产者。
- 初始值 = 仓库容量
product_num
(产品数)- 初始值 = 0
- 消费前
sem_wait(product_num)
:没产品就阻塞。 - 生产后
sem_post(product_num)
:通知消费者有新货。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <semaphore.h>
#include <pthread.h>
#define NUM 10
int queue[NUM]; // 全局数组实现环形队列
sem_t blank_num, product_num; // 空格信号量,产品信号量
void *producer(void *arg)
{
int i = 0;
while (1) {
sem_wait(&blank_num); // 生产者将空格数--,为0则阻塞等待
queue[i] = rand() % 1000 + 1; // 生产一个产品
printf("-----Produce----%d\n", queue[i]);
sem_post(&product_num); // 将产品数++
i = (i + 1) % NUM; // 借助下标实现环形
sleep(rand() % 3);
}
}
void *consumer(void *arg)
{
int i = 0;
while (1) {
sem_wait(&product_num);
printf("----Consume----%d\n", queue[i]);
queue[i] = 0;
sem_post(&blank_num);
i = (i+1) % NUM;
sleep(rand()%3);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
sem_init(&blank_num, 0, NUM);
sem_init(&product_num, 0, 0);
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_num);
sem_destroy(&product_num);
return 0;
}
- 生产者:往缓冲区(
queue
)放数据。 - 消费者:从缓冲区取数据。
- 信号量控制:
blank_num
:当前可用空位的数量(初始值 = 缓冲区大小)。product_num
:当前已有产品的数量(初始值 = 0)。
- 环形队列:用数组下标
(i+1)%NUM
实现,避免移动数据。
四、哲学家就餐问题
-
经典哲学家问题的死锁场景:
- 5 个哲学家同时拿起左边的筷子。
- 所有人都在等待右边筷子导致永久阻塞。
-
解决方案:
- 资源顺序法:最后一个哲学家反向拿筷子(右手→左手),打破循环等待条件。
- 这样最多有 4 个哲学家能同时持有一根筷子,至少有一根可用。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#define N 5 // 哲学家/筷子数量
pthread_mutex_t chopstick[N]; // 筷子互斥锁
void think(int id) {
printf("哲学家 %d 正在思考...\n", id);
sleep(rand() % 3 + 1);
}
void eat(int id) {
printf("哲学家 %d 正在吃面...\n", id);
sleep(rand() % 2 + 1);
}
void* philosopher(void* arg) {
int id = *(int*)arg;
int left = id; // 左手筷子编号
int right = (id + 1) % N; // 右手筷子编号
while (1) {
think(id); // 思考
// 为了防止死锁:最后一个哲学家先拿右手,再拿左手
if (id == N - 1) {
pthread_mutex_lock(&chopstick[right]);
pthread_mutex_lock(&chopstick[left]);
} else {
pthread_mutex_lock(&chopstick[left]);
pthread_mutex_lock(&chopstick[right]);
}
eat(id); // 吃面
// 放下筷子
pthread_mutex_unlock(&chopstick[left]);
pthread_mutex_unlock(&chopstick[right]);
}
return NULL;
}
int main() {
pthread_t tid[N];
int id[N];
srand(time(NULL));
// 初始化筷子
for (int i = 0; i < N; i++) {
pthread_mutex_init(&chopstick[i], NULL);
}
// 创建哲学家线程
for (int i = 0; i < N; i++) {
id[i] = i;
pthread_create(&tid[i], NULL, philosopher, &id[i]);
}
// 等待线程结束(实际上是死循环,不会结束)
for (int i = 0; i < N; i++) {
pthread_join(tid[i], NULL);
}
// 销毁互斥锁
for (int i = 0; i < N; i++) {
pthread_mutex_destroy(&chopstick[i]);
}
return 0;
}