实际上我是从教材《现代操作系统》中了解到这种类型的问题,也叫做 I P C IPC IPC问题,有几个很著名的问题,还蛮有意思的,就特意写篇笔记记录一下 。其中我只关注信号量解决问题的逻辑。而不是具体的实现。
注意这里对信号量 s s s的的操作 P ( s ) , V ( s ) P(s),V(s) P(s),V(s)都具有原子性。
信号量的数值就是相当于可以访问共享数据区域的最大进程数,如果信号量为二元信号量 b i n a r y s e m a p h o r e binary \ semaphore binary semaphore,那么和互斥锁是一样的作用,通常用于严格限制的区域。如果信号量不是二元的,那么一般是用于对有限数量实例组成的资源的访问。
下面是我从一个视频看到的,挺有意思的。
互斥锁(二元信号量) 就像一把钥匙。谁拿到它就可以上厕所,但是只有一个人才能上厕所。所有人都是一个一个地去上厕所。
类似于文件的访问,只有一把钥匙,谁拿到那么谁就可以获取。
多元信号量 就像多把钥匙。谁拿到它就可以上厕所,允许多个人同时上厕所。
类似于文件的访问,有多把钥匙,谁拿到那么谁就可以获取文件资源。 最常见的应用就是利用信号量控制多线程的并发数量,假设我的设备支持最高 1000 1000 1000的线程并发,但是我设置信号量为 10 10 10,那么实际上最多并发运行的线程不会超过 10 10 10个。
消费者进程和生产者进程存在一个共享的数据区域。所以存在互斥的问题。所以使用二元信号量来严格限制两个进程的读写。
生产者:
void producer() {
P(&mutex); // 申请钥匙
createItem(); // 生产数据
V(&mutex) // 归还钥匙
}
消费者:
void producer() {
P(&mutex); // 申请钥匙
consumeItem(); // 消耗数据
V(&mutex) // 归还钥匙
}
消费者消费所有的数据之后,不能继续消费数据,所以这个时候要唤醒生产者,同理,当槽满了的时候, 不能继续生产数据,所以这个时候要唤醒消费者,所以存在同步的问题。同步的问题往往可以使用拓扑图的模式直观解决。
生产者:
void produce() {
while(true) {
P(&slot); // 查看是否还有空的槽,没有的话就停止阻塞
P(&mutex);
createItem();
V(&mutex);
V(&items); // 把数据的数量增加一
}
}
消费者:
void consume() {
while(true) {
P(&item); // 查看是否还有数据,,没有的话就阻塞
P(&mutex);
consumeItem();
V(&mutex);
V(&slot); // 把空的槽的槽的数量加一
}
}
C o d e \color{Blue}{Code} Code
#include
#include
#include
#include
#include
typedef struct {
int *buf; // 缓存区的大小
int n; // 最大的item数量
int front; // 第一个item
int rear; // 最后一个item
sem_t mutex; // 锁变量
sem_t slots; // 空的槽数
sem_t items; // item的数量
} sbuf_t;
void *sbuf_insert(void *arg) {
sbuf_t* sp = (sbuf_t*) arg;
printf("start produce\n");
while(1) {
sem_wait(&sp->slots);
sem_wait(&sp->mutex);
sp->buf[(++sp->rear) % (sp->n)] = 1;
printf("Insert successfully!\n");
sem_post(&sp->mutex);
sem_post(&sp->items);
}
}
void *sbuf_remove(void *arg) {
sbuf_t* sp = (sbuf_t*) arg;
printf("start consume\n");
while(1) {
int item;
sem_wait(&sp->items);
sem_wait(&sp->mutex);
item = sp->buf[(++sp->front) % (sp->n)];
printf("%d\n", item);
sem_post(&sp->mutex);
sem_post(&sp->slots);
}
}
int main() {
pthread_t pro, con;
sbuf_t temp;
temp.buf = (int*)malloc(2 * sizeof(int));
temp.n = 2;
temp.front = temp.rear = 0;
sem_init(&temp.mutex, 0, 1);
sem_init(&temp.slots, 0, 2);
sem_init(&temp.items, 0, 0);
sbuf_t* S = &temp;
pthread_create(&pro, 0, sbuf_insert, (void*)S);
printf("thread producer is done!\n");
pthread_create(&con, 0, sbuf_remove, (void*)S);
printf("thread consumer is done!\n");
sleep(1000);
}
这里使用创建五个线程表示五个哲学家:(因为一个哲学家本质就是一个同时的生产者和消费者,所以把生产者函数与消费者函数放在一起,叉子就是数据项)
void philosopher(int id) { // id表示是哪位哲学家
while(true) {
think();
take_forks();
eat();
put_forks();
}
}
其中我们关心的只有拿叉子和放叉子的过程,因为一把叉子不可能同时拿起来和放下去所以存在互斥的问题。(本质就是生产者和消费者的问题)
拿叉子:
void take_forks(int id) { // id表示的哲学家的位置
while(true) {
P(&mutex);
getforks();
V(&mutex);
}
}
放叉子:
void put_forks(int id) { // id表示的哲学家的位置
while(true) {
P(&mutex);
backforks();
V(&mutex);
}
}
叉子必须要有的时候才可以拿,同理位置必须是空的时候才可以放。但是这里的位置容量为 1 1 1,所以不用判断是否什么时候可以放,这是一个同步的问题。注意这里的代码的 I D ID ID我使用了数字来表示哲学家和筷子的位置,总共为 10 10 10。
拿叉子:
void take_forks(int id) {
while(true) {
P(&s[(id+1)%10]); // 判断相邻的哲学家有没有筷子,没有就等待别人吃完
P(&s[(id-1+10)%10]);
P(&mutex);
getforks();
V(&mutex);
}
}
放叉子:
void put_forks(int id) {
while(true) {
P(&mutex);
backforks();
V(&mutex);
V(&s[(id+1)%10]); // 放下筷子
V(&s[(id-1+10)%10]);
}
}
教材的解法使用了状态表示,这样就可以免去 I D ID ID的编码。其实很多的有限资源竞争的问题本质就是生产者-消费者模型的拓展。下面的代码也使用了标准的解法:
C o d e \color{Blue}{Code} Code
#include
#include
#include
#include
#include
#define LEFT ((id + 5 - 1) % 5)
#define RIGHT ((id + 1) % 5)
#define THINKING 0
#define HUNGRY 1
#define EATING 2
sem_t mutex;
sem_t phi[5];
int state[5];
void take_forks(int);
void put_forks(int);
void test(int);
void *Philosopher(void* arg) {
int *temp = (int *)arg;
while(1) {
printf("%d is thinking.\n", *temp);
take_forks(*temp);
printf("%d is eating.\n", *temp);
put_forks(*temp);
}
}
void take_forks(int id) {
sem_wait(&mutex);
state[id] = HUNGRY;
test(id);
sem_post(&mutex);
sem_wait(&phi[id]);
printf("%d get the forks.\n", id);
}
void put_forks(int id) {
sem_wait(&mutex);
state[id] = THINKING;
printf("%d puts the forks\n", id);
test(LEFT);
test(RIGHT);
sem_post(&mutex);
}
void test(int id) {
if(state[id] == HUNGRY && state[LEFT] != EAGAIN && state[RIGHT] != EAGAIN) {
state[id] = EATING;
sem_post(&phi[id]);
}
}
int main() {
pthread_t thread[5];
sem_init(&mutex, 0, 1);
for(int i = 0; i < 5; i++) sem_init(&phi[i], 0, 0);
int *p = (int *)malloc(5 * sizeof(int));
for(int i = 0; i < 5; i++) p[i] = i;
for(int i = 0; i < 5; i++) {
pthread_create(&thread[i], 0, Philosopher, (void*)(p+i));
}
sleep(50);
}
这里假设读者的优先级大于写者。其实这里还有一个细节的问题,那就是所有的相同类型的进程不应该一个一个进去,所有的读进程应该可以同时进去,而写操作则不一定。
读者:
void reader() {
while(true) {
P(&mutex);
read();
V(&mutex);
}
}
写者:
void writer() {
while(true) {
P(&mutex);
write();
V(&mutex);
}
}
但是这里的读者并不止有一个,所以为了保证读操作优先级更高,我们规定写者只能一个一个进来,读者只要是"读"模式就可以进来,只有读完了,才能写。使用拓扑的表达就是下面这样:
读者:
void reader_in() {
while(true) {
P(&mutex);
readcnt++; // 通过readcnt可以实现读者不用一个一个进来,而是用一个数字表达所有等待的读者进程
if(readcnt == 1) P(&w); // 有读者进程,获得权限
V(&mutex);
}
}
void reader_out() {
while(true) {
P(&mutex);
readcnt--;
if(readcnt == 0) V(&w); // 读者没了,激活写者
V(&mutex);
}
}
写者:
void writer() {
while(true) {
P(&w) // 等待写入
...
write();
...
v(&w)
}
}
这里可以观察到信号量 w w w保证了读者和写者的互斥,信号量 m u t e x mutex mutex保证了共享数据 r e a d c n t readcnt readcnt的正确性。(由于使用了无限循环,也就是有无数个读者,会产生饥饿现象。)
C o d e \color{Blue}{Code} Code
#include
#include
#include
#include
#include
#define true 1
sem_t mutex;
sem_t w;
int readcnt;
void reader_in(int);
void reader_out(int);
void *reader(void* arg) {
int *temp = (int *)arg;
while(true) {
reader_in(*temp);
reader_out(*temp);
}
}
void reader_in(int id) {
while(true) {
sem_wait(&mutex);
readcnt++;
printf("A reader in.\n");
if(readcnt == 1) sem_wait(&w);
sem_post(&mutex);
}
}
void reader_out(int id) {
while(true) {
sem_wait(&mutex);
readcnt--;
printf("A reader out.\n");
if(readcnt == 0) sem_post(&w);
sem_post(&mutex);
}
}
void* writer(void* arg) {
int * temp = (int *) arg;
while(true) {
sem_wait(&w);
printf("SUCCESS write data %d\n", *temp);
sem_post(&w);
}
}
int main() {
pthread_t r_thread[10];
pthread_t w_thread[2];
sem_init(&mutex, 0, 1);
sem_init(&w, 0, 1);
int *p = (int *)malloc(12 * sizeof(int));
for(int i = 0; i < 12; i++) p[i] = i;
for(int i = 0; i < 10; i++) pthread_create(&r_thread[i], 0, reader, (void*)(p+i));
for(int i = 0; i < 2; i++) pthread_create(&w_thread[i], 0, writer, (void*)(p+i+10));
sleep(100);
}