信号量解决同步互斥问题

同步互斥问题的产生

实际上我是从教材《现代操作系统》中了解到这种类型的问题,也叫做 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,那么和互斥锁是一样的作用,通常用于严格限制的区域。如果信号量不是二元的,那么一般是用于对有限数量实例组成的资源的访问。

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

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

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
类似于文件的访问,有多把钥匙,谁拿到那么谁就可以获取文件资源。 最常见的应用就是利用信号量控制多线程的并发数量,假设我的设备支持最高 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 <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#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 <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#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);
}

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

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
信号是一种用于进程间同步互斥的工具。它是一个计数器,用于控制多个进程对共享资源的访问。当一个进程需要访问共享资源时,它必须先获取信号,然后才能访问该资源。当进程访问完共享资源后,它必须释放信号,以便其他进程可以访问该资源。 在使用信号解决同步问题时,通常需要定义两个信号:一个用于表示资源的可用数,另一个用于表示等待访问该资源的进程数。 下面是使用信号解决生产者-消费者问题的示例代码: ``` #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #define BUFFER_SIZE 10 int buffer[BUFFER_SIZE]; sem_t empty, full; pthread_mutex_t mutex; void *producer(void *arg) { int item; while (1) { item = rand() % 100; sem_wait(&empty); pthread_mutex_lock(&mutex); buffer[insert_index] = item; insert_index = (insert_index + 1) % BUFFER_SIZE; pthread_mutex_unlock(&mutex); sem_post(&full); } } void *consumer(void *arg) { int item; while (1) { sem_wait(&full); pthread_mutex_lock(&mutex); item = buffer[remove_index]; remove_index = (remove_index + 1) % BUFFER_SIZE; pthread_mutex_unlock(&mutex); sem_post(&empty); printf("Consumed item: %d\n", item); } } int main() { pthread_t producer_thread, consumer_thread; sem_init(&empty, 0, BUFFER_SIZE); sem_init(&full, 0, 0); pthread_mutex_init(&mutex, NULL); pthread_create(&producer_thread, NULL, producer, NULL); pthread_create(&consumer_thread, NULL, consumer, NULL); pthread_join(producer_thread, NULL); pthread_join(consumer_thread, NULL); sem_destroy(&empty); sem_destroy(&full); pthread_mutex_destroy(&mutex); return 0; } ``` 在这段代码中,使用了两个信号 empty 和 full 分别表示缓冲区空闲数和已占用数。当生产者向缓冲区中插入数据时,需要先获取 empty 信号,如果缓冲区已满,则阻塞等待;同时需要获取互斥锁 mutex,以保证对缓冲区的访问同步。当生产者插入数据后,需要释放互斥锁 mutex 和 full 信号,以通知消费者有新的数据可用。 当消费者从缓冲区中取出数据时,需要获取 full 信号,如果缓冲区为空,则阻塞等待;同时需要获取互斥锁 mutex,以保证对缓冲区的访问同步。当消费者取出数据后,需要释放互斥锁 mutex 和 empty 信号,以通知生产者有新的空闲位置可用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值