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)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • Q:为什么出现多线程? A:为了实现同时干多件事的需求(并发),同时进行着下载和页面UI刷新。对于处理器,为每个线...
    幸福相依阅读 1,538评论 0 2
  • 简介 线程创建 线程属性设置 线程参数传递 线程优先级 线程的数据处理 线程的分离状态 互斥锁 信号量 一 线程创...
    第八区阅读 8,393评论 1 6
  • 摘要 线程概念,线程与进程的区别与联系学会线程控制,线程创建,线程终止,线程等待了解线程分离与线程安全学会线程同步...
    狼之足迹阅读 443评论 2 3
  • OSSpinLock OSSpinLock 不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(b...
    GAME666阅读 1,267评论 0 0
  • 线程概念 典型的UNIX进程可以看作只有一个控制线程,任务的执行只能串行来做。有了多个控制线程后,就可以同时做多个...
    pangqiu阅读 322评论 0 2