信号量解决同步互斥问题

同步互斥问题的产生

实际上我是从教材《现代操作系统》中了解到这种类型的问题,也叫做 I P C IPC IPC问题,有几个很著名的问题,还蛮有意思的,就特意写篇笔记记录一下 。其中我只关注信号量解决问题的逻辑。而不是具体的实现。


一些概念的理解:

  • 临界区域:就是两个进程之间共享的数据区域,进程都可以对其进行读写。
  • 信号量:实际上就是一个计数器,表示的是一种权限资源。
  • P ( s ) P(s) P(s):如果 s > 0 s>0 s>0,那么 P P P就会把 s s s 1 1 1,如果 s = 0 s=0 s=0那么这个进程就会被挂起,执行其他的进程。
  • V ( s ) V(s) V(s) V V V会把 s s s 1 1 1

注意这里对信号量 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,那么和互斥锁是一样的作用,通常用于严格限制的区域。如果信号量不是二元的,那么一般是用于对有限数量实例组成的资源的访问。

下面是我从一个视频看到的,挺有意思的。

互斥锁(二元信号量) 就像一把钥匙。谁拿到它就可以上厕所,但是只有一个人才能上厕所。所有人都是一个一个地去上厕所。

信号量解决同步互斥问题_第1张图片
信号量解决同步互斥问题_第2张图片

信号量解决同步互斥问题_第3张图片

类似于文件的访问,只有一把钥匙,谁拿到那么谁就可以获取。

信号量解决同步互斥问题_第4张图片

多元信号量 就像多把钥匙。谁拿到它就可以上厕所,允许多个人同时上厕所。

信号量解决同步互斥问题_第5张图片

信号量解决同步互斥问题_第6张图片

信号量解决同步互斥问题_第7张图片
信号量解决同步互斥问题_第8张图片
类似于文件的访问,有多把钥匙,谁拿到那么谁就可以获取文件资源。 最常见的应用就是利用信号量控制多线程的并发数量,假设我的设备支持最高 1000 1000 1000的线程并发,但是我设置信号量为 10 10 10,那么实际上最多并发运行的线程不会超过 10 10 10个。


使用信号量解决问题

一、生产者和消费者问题

信号量解决同步互斥问题_第9张图片

消费者进程和生产者进程存在一个共享的数据区域。所以存在互斥的问题。所以使用二元信号量来严格限制两个进程的读写。

生产者:

void producer() {
	  P(&mutex); // 申请钥匙
	  createItem(); // 生产数据
	  V(&mutex) // 归还钥匙
}

消费者:

void producer() {
	  P(&mutex); // 申请钥匙
	  consumeItem(); // 消耗数据
	  V(&mutex) // 归还钥匙
}

消费者消费所有的数据之后,不能继续消费数据,所以这个时候要唤醒生产者,同理,当槽满了的时候, 不能继续生产数据,所以这个时候要唤醒消费者,所以存在同步的问题。同步的问题往往可以使用拓扑图的模式直观解决。

信号量解决同步互斥问题_第10张图片

生产者:

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);
}

二、竞争有限资源类型问题(哲学家就餐问题和生产者-消费者问题)

哲学家就餐问题:(五个哲学家一直在思考吃饭,吃饭的时候必须要两个叉子。思考的时候会把叉子全部放下去。)

信号量解决同步互斥问题_第11张图片

这里使用创建五个线程表示五个哲学家:(因为一个哲学家本质就是一个同时的生产者和消费者,所以把生产者函数与消费者函数放在一起,叉子就是数据项

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);
	}
}

但是这里的读者并不止有一个,所以为了保证读操作优先级更高,我们规定写者只能一个一个进来,读者只要是"读"模式就可以进来,只有读完了,才能写。使用拓扑的表达就是下面这样

信号量解决同步互斥问题_第12张图片

读者:

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);
}

其他的问题都可以通过这三个模型推导得到,因此掌握这三个基本模型十分重要。文章写作不易,转载标明出处。

你可能感兴趣的:(算法)