一文解决经典多线程互斥和同步问题,妈妈再也不用担心死锁问题了
前言
在开始之前,我们要明白互斥和同步是不同的概念,简单来说,互斥就是同一时间,某个资源只能运行一个访问者访问;同步指的是某些操作在时间上具有一定顺序,例如A操作必须在B操作之后,在很多时候,同步需要借助互斥。
生产者-消费者问题
问题描述:一组生产者和一组消费者线程共享一个初始为空,长度为n
的缓冲区,对于生产者,只有当缓冲区不满时候,才能生产数据放到缓冲区中,对于消费者,只有当缓冲区不空时,才能从缓冲区中读取数据进行消费。
**分析:**由于缓冲区是临界资源,因此每次只有一个生产者或者消费者访问,我们需要个互斥变量来完成互斥访问逻辑,对于生产者,我们需要获取目前缓冲区空槽数目,当缓冲区满了时候需要将线程阻塞,对于消费者,我们需要获取目前缓冲区存在的数据个数,当缓冲区空了时候要将线程阻塞。
我们可以借助信号量完成,简化源码如下:
seamphore mutex = 1;
seamphore slot=n;
seamphore data=0;
producer(){
while(1){
produce an item in nextp;
// 注意,我们要判断有没有插槽,再去拿互斥锁
// 如果我们先拿互斥锁,那么当缓冲区满了之后,假如生产者先拿到互斥锁,再去等待插槽,那么会产生死锁
P(slot);
P(mutex);
produce an item to buffer;
V(mutex);
V(data);
}
}
consumer(){
while(1){
P(data);
P(mutex);
consume an item from buffer;
V(mutex);
V(slot);
}
}
C语言实现如下:
#include<stdio.h>
#include<semaphore.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>
typedef struct{
int size, top;
int buf[10];
}Buf;
Buf buffer;
sem_t mutex, slot, data;
void* consumer(char *name){
while(1){
sem_wait(&data);
sem_wait(&mutex);
int number = buffer.buf[--buffer.top];
// 休眠,模拟消费数据需要时间
sleep(1);
printf("%s consume number : %d\n", name, number);
sem_post(&mutex);
sem_post(&slot);
}
}
void* producer(char *name){
while(1){
// 注意,我们要判断有没有插槽,再去拿互斥锁
// 如果我们先拿互斥锁,那么当缓冲区满了之后,假如生产者先拿到互斥锁,再去等待插槽,那么会产生死锁
sem_wait(&slot);
sem_wait(&mutex);
// 产生一个[0, 99]范围之内的数字
int number = rand() % 100;
// 休眠,模拟产生数据需要时间
sleep(1);
buffer.buf[buffer.top++] = number;
printf("%s produce number : %d\n", name, number);
sem_post(&mutex);
sem_post(&data);
}
}
int main(){
int bufSize = 5;
srand((unsigned)time(NULL));
// 三个生产者,两个消费者
pthread_t producerTh1, producerTh2, producerTh3;
pthread_t consumerTh1, consumerTh2;
// 初始化缓冲区
buffer.size = bufSize, buffer.top = 0;
// 初始化信号量 mutex = 1, slot = bufSize, data = 0
// 第一个参数不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享
sem_init(&mutex, 0, 1);
sem_init(&slot, 0, bufSize);
sem_init(&data, 0, 0);
// 创建生产者和消费者
pthread_create(&producerTh1,NULL, producer, "producer-1");
pthread_create(&producerTh2,NULL, producer, "producer-2");
pthread_create(&producerTh3,NULL, producer, "producer-3");
pthread_create(&consumerTh1,NULL, consumer, "consumer-1");
pthread_create(&consumerTh2,NULL, consumer, "consumer-2");
// 主线程等待其他线程结束
pthread_join(producerTh1,NULL);
pthread_join(producerTh2,NULL);
pthread_join(producerTh3,NULL);
pthread_join(consumerTh1,NULL);
pthread_join(consumerTh2,NULL);
return 0;
}
哲学家进餐问题
问题描述:n
个哲学家坐在一起,围成一个圈,每个哲学家之间放着一根筷子,每时每刻,每个哲学家只做两件事情,要么吃饭,要么思考,吃饭时要同时把左右手的筷子拿起才能吃饭(左右手的筷子分开拿),思考时放下手中的筷子,如何保证「吃饭」有序进行。
**分析:**如果我们不做任何限制,那么当每个哲学家都尝试获取筷子时,有可能每个人都拿到了一根,然后都在等待别人放下另一根,从而陷入死锁。解决方法有三种:
- 最多允许
n-1
个哲学家同时拿起筷子,那么必然有一个是可以拿起两根筷子的。 - 给每个哲学家编号,奇数的拿起筷子的顺序是先左后右,偶数刚好相反
- 对于每个哲学家,仅当其左右手的筷子都没人拿时候才开始拿筷子
我们演示第二个方法,借助信号量,我们简化的代码如下(假设有 5 个哲学家):
seamphore chopsticks = {1,1,1,1};
philospher(id){
while(1){
think();
// 奇数哲学家
if(id % 2 == 1){
P(chopsticks[i]);
P(chopsticks[(i + 1) % 5]);
eat();
V(chopsticks[(i + 1) % 5]);
V(chopsticks[i]);
} else{
// 偶数哲学家拿起筷子顺序刚好相反
}
}
}
C语言实现如下:
#include<stdio.h>
#include<semaphore.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>
#define N 5
sem_t chopsticks[N];
int ids[N];
philospher(int *id){
while(1){
// think();
printf("philospher %d thinking ....\n", *id);
sleep(1);
// 奇数哲学家
if(*id % 2 == 1){
sem_wait(&chopsticks[*id]);
sem_wait(&chopsticks[(*id + 1) % 5]);
// eat();
printf("philospher %d eating ....\n", *id);
sem_post(&chopsticks[(*id + 1) % 5]);
sem_post(&chopsticks[*id]);
} else{
// 偶数哲学家拿起筷子顺序刚好相反
sem_wait(&chopsticks[(*id + 1) % 5]);
sem_wait(&chopsticks[*id]);
// eat();
printf("philospher %d eating ....\n", *id);
sem_post(&chopsticks[*id]);
sem_post(&chopsticks[(*id + 1) % 5]);
}
}
}
int main(){
// 五个哲学家
pthread_t philospherThs[N];
for(int i = 0; i < N;i++){
ids[i] = i;
}
// 初始化信号量
for(int i = 0; i < N;i++){
// 第一个参数不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享
sem_init(&chopsticks[i], 0, 1);
}
// 创建哲学家进程
for(int i = 0; i < N;i++){
pthread_create(&philospherThs[i],NULL, philospher, &ids[i]);
}
// 主线程等待其他线程结束
for(int i = 0; i < N;i++){
pthread_join(philospherThs[i], NULL);
}
return 0;
}
读-写者问题
问题描述:一组读者和一组写者线程共享内存区域,由于写者会改变内存区域,使得读者可能读取到的是脏数据,因此写者与写者之间、写者和读者之间必须互斥访问内存区域,但是读者之间无需互斥访问。这类问题一般可以抽象成两类变种:
- 读者优先:如果有
n
个读者在访问内存,当写者想要访问时,必须当所有的读者退出内存区,写者才能访问,并且在读者读取内存过程中,如果有新的读者加进来,那么写者将继续等待。 - 写者优先:如果有
n
个写者在访问内存(有n-1
个在等待),当读者想要访问时,必须当所有的写者退出内存区(所有的写者的「写」任务都完成了),读者才能访问,并且在写者读取内存过程中,如果有新的写者加进来(新的写者加入到等待队列),那么读者将继续等待。
分析:我们以第一种变体为例,我们需要读者和写者都去竞争访问内存这个权限,由于是读者优先,只有最后一个读者才需要把这个权限让出去,因此我们要维护读者个数。
简化的代码如下:
// 初始值为 0
int readerCnt;
// 互斥访问 readerCnt,初始值为 1
seamphore mutex;
// 内存访问权限,初始值为 1
seamphore w;
reader(){
while(1){
P(mutex);
readerCnt++;
// 如果是第一个读者,则去竞争权限
if(readerCnt == 1){
P(w);
}
V(mutex);
read();
P(mutex);
readerCnt--;
// 如果是最后一个读者,则释放权限
if(readerCnt == 0){
V(w);
}
V(mutex);
}
}
writer(){
while(1){
P(w);
write();
V(w);
}
}
C语言实现如下:
#include<stdio.h>
#include<semaphore.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>
// 初始值为 0
int readerCnt;
// 互斥访问 readerCnt,初始值为 1
sem_t mutex;
// 内存访问权限,初始值为 1
sem_t w;
void reader(char *name){
while(1){
sem_wait(&mutex);
readerCnt++;
// 如果是第一个读者,则去竞争权限
if(readerCnt == 1){
sem_wait(&w);
}
sem_post(&mutex);
printf("%s reading...\n", name);
sleep(1);
sem_wait(&mutex);
readerCnt--;
// 如果是最后一个读者,则释放权限
if(readerCnt == 0){
sem_post(&w);
}
sem_post(&mutex);
sleep(3);
}
}
void writer(char *name){
while(1){
sem_wait(&w);
printf("%s write...\n", name);
sleep(2);
sem_post(&w);
sleep(1);
}
}
int main(){
// 三个读者,两个写者
pthread_t readerTh1, readerTh2, readerTh3;
pthread_t writerTh1, writerTh2;
// 初始化信号量
sem_init(&mutex, 0, 1);
sem_init(&w, 0, 1);
// 创建读者和写者
pthread_create(&readerTh1,NULL, reader, "reader-1");
pthread_create(&readerTh2,NULL, reader, "reader-2");
pthread_create(&readerTh3,NULL, reader, "reader-3");
pthread_create(&writerTh1,NULL, writer, "writer-1");
pthread_create(&writerTh2,NULL, writer, "writer-2");
// 主线程等待其他线程结束
pthread_join(readerTh1,NULL);
pthread_join(readerTh2,NULL);
pthread_join(readerTh3,NULL);
pthread_join(writerTh1,NULL);
pthread_join(writerTh2,NULL);
return 0;
}
本文由「黄阿信」创作,创作不易,请多支持。
如果您觉得本文写得不错,那就点一下「赞赏」请我喝杯咖啡~
商业转载请联系作者获得授权,非商业转载请附上原文出处及本链接。
关注公众号,获取最新动态!