要么改变世界,要么适应世界

一文解决经典多线程互斥和同步问题,妈妈再也不用担心死锁问题了

2024-03-31 12:01:09
88
目录

前言

在开始之前,我们要明白互斥和同步是不同的概念,简单来说,互斥就是同一时间,某个资源只能运行一个访问者访问;同步指的是某些操作在时间上具有一定顺序,例如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个哲学家坐在一起,围成一个圈,每个哲学家之间放着一根筷子,每时每刻,每个哲学家只做两件事情,要么吃饭,要么思考,吃饭时要同时把左右手的筷子拿起才能吃饭(左右手的筷子分开拿),思考时放下手中的筷子,如何保证「吃饭」有序进行。

**分析:**如果我们不做任何限制,那么当每个哲学家都尝试获取筷子时,有可能每个人都拿到了一根,然后都在等待别人放下另一根,从而陷入死锁。解决方法有三种:

  1. 最多允许n-1个哲学家同时拿起筷子,那么必然有一个是可以拿起两根筷子的。
  2. 给每个哲学家编号,奇数的拿起筷子的顺序是先左后右,偶数刚好相反
  3. 对于每个哲学家,仅当其左右手的筷子都没人拿时候才开始拿筷子

我们演示第二个方法,借助信号量,我们简化的代码如下(假设有 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;
}

读-写者问题

问题描述:一组读者和一组写者线程共享内存区域,由于写者会改变内存区域,使得读者可能读取到的是脏数据,因此写者与写者之间、写者和读者之间必须互斥访问内存区域,但是读者之间无需互斥访问。这类问题一般可以抽象成两类变种:

  1. 读者优先:如果有n个读者在访问内存,当写者想要访问时,必须当所有的读者退出内存区,写者才能访问,并且在读者读取内存过程中,如果有新的读者加进来,那么写者将继续等待。
  2. 写者优先:如果有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;
}
历史评论
开始评论