重生之我学操作系统——第31章 信号量
回顾:
临界段问题
- 在并发程序中,资源是共享的
- 临界段资源:部分资源需要互斥使用
- 利用“锁”的思想保护临界段资源
并发的需求
- 互斥执行、同步执行
一、信号量的定义
控制并发的信号灯-信号量(semaphore)
- 信号量是有一个整数值的对象
- 信号量可以代替锁/条件变量
- 信号量可以并发过程中控制资源的使用情况
信号量是包含一个整数值的对象。
- 我们可以用sem_wait()和sem_post()来操作
- 信号量的初始值决定其行为,因此需要初始化
- 例:初始化为1,第二个参数0标识信号量是线程共享的
#include <semaphore.h>
sem_t s;
sem_init(&s, 0, 1); // initialize s to the value 1
sem_wait():又称为P()操作
- 信号量的值减1
- 结果如果小于0,则使调用的线程挂起直到被其他线程调用 sem_post()唤醒
int sem_wait(sem_t *s) { decrement the value of semaphore s by one; wait if value of semaphore s is negative }
sem_post():又称为V()操作
- 将信号量的值加1
- 如果有其他线程等待被唤醒,则唤醒其中一个
二、二值信号量(锁)
用信号量作为锁
- 将临界区环绕
sem_t m; sem_init(&m, 0, X); // initialize semaphore to X; what should X be? sem_wait(&m); //critical section here sem_post(&m);
X = 1
三、信号量作为条件变量
一个线程等待条件成立,另一个线程修改条件并发信号
- 用信号量值的变化作为状态改变信号
#include <stdio.h> #include <pthread.h> #include <semaphore.h> sem_t s; // 定义信号量 void *child(void *arg) { printf("child\n"); sem_post(&s); // signal here: child is done return NULL; } int main(int argc, char *argv[]) { sem_init(&s, 0, X); // X 应该是 0,表示信号量初始值为 0 printf("parent: begin\n"); pthread_t c; pthread_create(&c, NULL, child, NULL); // 创建子线程 sem_wait(&s); // wait here for child printf("parent: end\n"); return 0; }
X = 0
四、生产者/消费者问题
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAX 10 // 缓冲区的最大大小
int buffer[MAX];
int fill = 0;
int use = 0;
sem_t empty; // 记录空槽位的信号量
sem_t full; // 记录满槽位的信号量
void put(int value) {
buffer[fill] = value; // 将值放入缓冲区
fill = (fill + 1) % MAX; // 更新填充指针
}
int get() {
int tmp = buffer[use]; // 从缓冲区取出值
use = (use + 1) % MAX; // 更新使用指针
return tmp;
}
void *producer(void *arg) {
int i;
for (i = 0; i < 10; i++) { // 假设生产 10 个产品
sem_wait(&empty); // 等待空槽位
put(i); // 生产产品
sem_post(&full); // 增加满槽位的计数
}
}
void *consumer(void *arg) {
int i, tmp = 0;
while (tmp != -1) {
sem_wait(&full); // 等待满槽位
tmp = get(); // 消费产品
sem_post(&empty); // 增加空槽位的计数
printf("%d\n", tmp); // 打印消费的产品
}
}
int main(int argc, char *argv[]) {
// 初始化信号量
sem_init(&empty, 0, MAX); // 初始化:MAX 个缓冲区空
sem_init(&full, 0, 0); // 初始化:0 个缓冲区满
// 创建生产者和消费者线程
pthread_t prod_thread, cons_thread;
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
// 等待线程结束
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
// 销毁信号量
sem_destroy(&empty);
sem_destroy(&full);
return 0;
}
是否可行?在什么情况下可能 产生问题?
一个简单规则:别忘了加锁
sem_t empty; // 记录空槽位的信号量
sem_t full; // 记录满槽位的信号量
sem_t mutex; // 互斥信号量,用于保护临界区
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) { // 假设 loops 是预定义的循环次数
sem_wait(&mutex); // 进入临界区
sem_wait(&empty); // 等待空槽位
put(i); // 生产产品
sem_post(&full); // 增加满槽位的计数
sem_post(&mutex); // 离开临界区
}
}
void *consumer(void *arg) {
int i;
for (i = 0; i < loops; i++) { // 假设 loops 是预定义的循环次数
sem_wait(&full); // 等待满槽位
sem_wait(&mutex); // 进入临界区
int tmp = get(); // 从缓冲区获取产品
sem_post(&empty); // 增加空槽位的计数
sem_post(&mutex); // 离开临界区
printf("%d\n", tmp); // 打印消费的产品
}
}
还有问题吗?
将锁精准的加到临界区附近。
sem_t empty; // 记录空槽位的信号量
sem_t full; // 记录满槽位的信号量
sem_t mutex; // 互斥信号量,用于保护临界区
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) { // 假设 loops 是预定义的循环次数
sem_wait(&empty); // line p1: 等待空槽位
sem_wait(&mutex); // line p1.5: 进入临界区,确保互斥访问
put(i); // line p2: 生产产品并放入缓冲区
sem_post(&mutex); // line p2.5: 离开临界区
sem_post(&full); // line p3: 增加满槽位的计数
}
}
五、读者/写者锁
- 一个线程写的时候,其他线程不能读或写
- 一个线程读的时候,其他线程可以读
- 例:链表操作
- insert():插入节点(写)
- lookup():查找节点(读)
“信号量”小结
- 控制共享资源的访问:互斥/同步
- 信号量作为锁/条件变量
- 生产者/消费者问题:2个信号量,分别描述空/满的缓冲块 数量,不能同时放或取
- 读者/写者问题:1个读锁,1个写锁,1个计数器