c++多线程生产者消费者模型以及读写锁实现

本文首发于我的公众号:码农手札,主要介绍linux下c++开发的知识包括网络编程的知识同时也会介绍一些有趣的算法题,欢迎大家关注,利用碎片时间学习一些编程知识,冰冻三尺非一日之寒,让我们一起加油!

前言

最近在学习操作系统的知识,又看到了经典的并发模型,不得不说在多线程编程中,最好使用一些已经被验证过的正确的模型,其中生产者消费者模型就是典型的成功模型,值得学习,其实之前我也写过生产者消费者的实现,但这次我会稍微深入一些,为什么要这样写,如果不这样写会带来什么样的问题。

简单但是有问题的例子

int buffer;
int count = 0;

void put(int value){
  assert(count == 0);
  count = 1;
  buffer = value;
}

int get(){
  assert(count == 1);
  count = 0;
  return buffer;
}

上面是put()和get()函数的实现

cond_t cond;
mutex_t mutex;

void *producer(void *arg){
  int i ;
  int loops = (int)arg;
  for(i = 0; i < loops; ++i){
    pthread_mutex_lock(&mutex);         //p1
    if(count == 1)                      //p2
      pthread_cond_wait(&cond, &mutex); //p3
    put(i);                             //p4
    pthread_cond_signal(&cond);         //p5
    pthread_mutex_unlock(&mutex);       //p6
  }
}

void *consumer(void *arg){
  int i ;
  int loops = (int)arg;
  for(i = 0; i < loops; ++i){
    pthread_mutex_lock(&mutex);         //c1
    if(count == 0)                      //c2
      pthread_cond_wait(&cond, &mutex); //c3
    int tmp = get();                    //c4
    pthread_cond_signal(&cond);         //c5
    pthread_mutex_unlock(&mutex);       //c6
    printf("%d\n",tmp);                 //c7
  }
}

这个应该是最简单,最直接的生产者消费者实现了,但是这里有两个问题,让我们来一个一个解决,第一个问题就是当有超过一个消费者的时候,这个代码有个十分严重的问题,假设一个消费者在缓冲区为空的时候进行消费,很显然,这个消费者线程会被阻塞在pthread_cond_wait(注意阻塞的时候这个消费者是不持有锁的),这个时候假设一个生产者生产了资料放入缓冲,然后调用pthread_cond_sigal,之前阻塞的线程会被放入到就绪队列中,准备运行。但是假设这个时候有个新的消费者线程来了,我们叫它第三者吧,由于之前的消费者还没有从pthread_cond_wait中返回,以及之前的生产者已经完成生产,阻塞在pthread_cond_wait中(因为缓冲区已经满了),这个时候实际上锁是没有被任何线程持有的,所以这个第三者长驱直入,直接把生产者消费的消费掉了,然后大大咧咧的跑了,这个时候之前就绪的消费者被调度,调用get()结果发现缓冲区为空,直接触发了assert,导致错误。

引发这个问题的原因很简单,因为在第一个消费者被生产者唤醒之后但在其运行之前,缓冲区已经发生了变化(由于另外一个消费者线程的运行)。这个实际上意味着消费者收到被唤醒的信号仅仅意味着状态可能发生了变化(并不代表等待的事情一定发生了)。这个实际上是对唤醒信号语义的理解,解决这个问题的办法也不难,代码如下:

用while代替if的办法(仍有问题)

cond_t cond;
mutex_t mutex;

void *producer(void *arg){
  int i ;
  int loops = (int)arg;
  for(i = 0; i < loops; ++i){
    pthread_mutex_lock(&mutex);         //p1
    while(count == 1)                   //p2
      pthread_cond_wait(&cond, &mutex); //p3
    put(i);                             //p4
    pthread_cond_signal(&cond);         //p5
    pthread_mutex_unlock(&mutex);       //p6
  }
}

void *consumer(void *arg){
  int i ;
  int loops = (int)arg;
  for(i = 0; i < loops; ++i){
    pthread_mutex_lock(&mutex);         //c1
    while(count == 0)                   //c2
      pthread_cond_wait(&cond, &mutex); //c3
    int tmp = get();                    //c4
    pthread_cond_signal(&cond);         //c5
    pthread_mutex_unlock(&mutex);       //c6
    printf("%d\n",tmp);                 //c7
  }
}

用while代替if确实解决了之前的问题,但是这个实现仍然是有问题的,那么问题在哪呢?
想象下面的场景,假设有一个生产者线程P和两个消费者线程C1以及C2,假设两个消费者线程先运行,并且由于缓冲区为空所以都睡眠了,生产者P运行在缓冲区放入一个值之后唤醒了一个消费者假设是C1,之后进入睡眠,这个时候C1发现缓冲区有数据,就直接消费了,然后这个消费者调用了pthread_cond_signal(&cond),但是这个时候有个问题就是,应该唤醒哪个等待的线程呢?我们知道肯定应该唤醒生产者线程,但是如果它唤醒了C2(这个是绝对可能的,取决于等待队列的实现),这个时候问题就出现了,由于C2唤醒之后发现缓冲区为空,就直接继续调用pthread_cond_wait(&cond,&mutex),进入睡眠了,这个时候三个线程都处于睡眠的状态,显然这是个问题。

本质上这个问题是由于信号的不明确导致的(不过如果使用pthread_cond_broadcast也是可以的,但是在线程很多的情况下非常没有必要,因为我们只需要唤醒生产者即可),要解决问题,实际上信号需要做到消费者的信号只唤醒生产者,生产者的信号只唤醒消费者就好了。

简单但是正确的例子

cond_t empty, fill;
mutex_t mutex;

void *producer(void *arg){
  int i ;
  int loops = (int)arg;
  for(i = 0; i < loops; ++i){
    pthread_mutex_lock(&mutex);         //p1
    while(count == 1)                   //p2
      pthread_cond_wait(&emtpy, &mutex);//p3
    put(i);                             //p4
    pthread_cond_signal(&fill);         //p5
    pthread_mutex_unlock(&mutex);       //p6
  }
}

void *consumer(void *arg){
  int i ;
  int loops = (int)arg;
  for(i = 0; i < loops; ++i){
    pthread_mutex_lock(&mutex);         //c1
    while(count == 0)                   //c2
      pthread_cond_wait(&fill, &mutex); //c3
    int tmp = get();                    //c4
    pthread_cond_signal(&empty);        //c5
    pthread_mutex_unlock(&mutex);       //c6
    printf("%d\n",tmp);                 //c7
  }
}

总结

这里简单说明了生产者消费者模型的几个小细节,之前我也写过c++中如何实现一个生产者消费者模型,链接在这里:c++生产者消费者模型实现

简单提一句的是总是对条件变量使用while而不是if,使用while循环也解决了假唤醒的情况,在某些线程库中,由于不同的实现,一个信号可能会唤醒两个线程,因此再次检查线程的等待条件是正确的操作。

参考文献:
操作系统导论(Operating Systems:Three Easy Pieces)

你可能感兴趣的:(c++多线程生产者消费者模型以及读写锁实现)