五个同步问题的经典模型之一:生产者/消费者问题

也叫缓存绑定问题(bounded- buffer),是一个经典的、多进程同步问题。

单生产者和单消费者

有两个进程:一组生产者进程和一组消费者进程共享一个初始为空、固定大小为n的缓存(缓冲区)。生产者的工作是制造一段数据,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待,如此反复; 同时,只有缓冲区不空时,消费者才能从中取出消息,一次消费一段数据(即将其从缓存中移出),否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。

问题的核心是:
1.要保证不让生产者在缓存还是满的时候仍然要向内写数据;
2.不让消费者试图从空的缓存中取出数据。

生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,他们也是同步关系。

  • 解决思路:对于生产者,如果缓存是满的就去睡觉。消费者从缓存中取走数据后就叫醒生产者,让它再次将缓存填满。若消费者发现缓存是空的,就去睡觉了。下一轮中生产者将数据写入后就叫醒消费者。
    不完善的解决方案会造成“死锁”,即两个进程都在“睡觉”等着对方来“唤醒”。

只有生产者和消费者两个进程,正好是这两个进程存在着互斥关系和同步关系。那么需要解决的是互斥和同步PV操作的位置。使用“进程间通信”,“信号标”semaphore就可以解决唤醒的问题:

我们使用了两个信号标:full 和 empty 。信号量mutex作为互斥信号量,它用于控制互斥访问缓冲池,互斥信号量初值为 1;信号量 full 用于记录当前缓冲池中“满”缓冲区数,初值为0。信号量 empty 用于记录当前缓冲池中“空”缓冲区数,初值为n。新的数据添加到缓存中后,full 在增加,而 empty 则减少。如果生产者试图在 empty 为0时减少其值,生产者就会被“催眠”。下一轮中有数据被消费掉时,empty就会增加,生产者就会被“唤醒”。

伪代码:

semaphore mutex=1; //临界区互斥信号量
semaphore empty=n;  //空闲缓冲区
semaphore full=0;  //缓冲区初始化为空
producer ()//生产者进程 
{
    while(1)
    {
        produce an item in nextp;  //生产数据
        P(empty);  //获取空缓冲区单元
        P(mutex);  //进入临界区.
        add nextp to buffer;  //将数据放入缓冲区
        V(mutex);  //离开临界区,释放互斥信号量
        V(full);  //满缓冲区数加1
    }
}

consumer ()//消费者进程
{
    while(1)
    {
        P(full);  //获取满缓冲区单元
        P(mutex);  // 进入临界区
        remove an item from buffer;  //从缓冲区中取出数据
        V (mutex);  //离开临界区,释放互斥信号量
        V (empty) ;  //空缓冲区数加1
        consume the item;  //消费数据
    }
}

该类问题要注意对缓冲区大小为n的处理,当缓冲区中有空时便可对empty变量执行P 操作,一旦取走一个产品便要执行V操作以释放空闲区。对empty和full变量的P操作必须放在对mutex的P操作之前。

1、若生产者进程已经将缓冲区放满,消费者进程并没有取产品,即 empty = 0,当下次仍然是生产者进程运行时,它先执行 P(mutex)封锁信号量,再执行 P(empty)时将被阻塞,希望消费者取出产品后将其唤醒。轮到消费者进程运行时,它先执行 P(mutex),然而由于生产者进程已经封锁 mutex 信号量,消费者进程也会被阻塞,这样一来生产者进程与消费者进程都
将阻塞,都指望对方唤醒自己,陷入了无休止的等待。
2、若消费者进程已经将缓冲区取空,即 full = 0,下次如果还是消费者先运行,也会出现类似的死锁。
不过生产者释放信号量时,mutex、full 先释放哪一个无所谓,消费者先释放 mutex 还是 empty 都可以。

多生产者多消费者

在多个制造商和多个消费者出现的情况下就会造成拥护不堪的情况,会导致两个或多个进程同时向一个磁道写入或读出数据。要理解这种情况是如何出现的,我们可以借助于putItemIntoBuffer()函数。它包含两个动作:一个来判断是否有可用磁道,另一个则用来向其写入数据。如果进程可以由多个制造商并发执行,下面的情况则会出现:
1、 两个制造商为emptyCount减值;
2、 一个制造商判断缓存中有可用磁道;
3、 第二个制造商与第一个制造商一样判断缓存中有可用磁道;
4、 两个制造商同时向同一个磁道写入数据。

多个生产者向一个缓冲区中存入数据,多个生产者从缓冲区中取数据。这是有界缓冲区问题,队列改写,生产者们之间、消费者们之间、生产者消费者之间互相互斥。
共享缓冲区作为一个环绕缓冲区,存数据到尾时再从头开始。

  • 我们使用一个互斥量保护生产者向缓冲区中存入数据。由于有多个生产者,因此需要记住现在向缓冲区中存入的位置。
  • 使用一个互斥量保护缓冲区中消息的数目,这个生产的数据数目作为生产者和消费者沟通的桥梁。
  • 使用一个条件变量用于唤醒消费者。由于有多个消费者,同样消费者也需要记住每次取的位置。

在选项中选择生产条目的数目,生产者的线程数目,消费者的线程数目。生产者将条目数目循环放入缓冲区中,消费者从缓冲区中循环取出并在屏幕上打印出来。
为了克服这个问题,我们需要一个方法,以确保一次只有一个制造商在执行调用函数。换个说法来讲,我们需要一个有“互斥信号标”(mutal exclusion)的“关键扇区”(critical section)。为了实现这一点,我们使用一个叫mutex二位信号标。因为一个二位信号标的值只能是1或0,只有一个进程能执行down(mutex)或up(mutex)。

代码:

#include "unp.h"
static const int NBUFF         = 10000;
static const int MAXNTHREADS = 100;
static int nitems;      //总共生产的条目数
static int buff[NBUFF]; //生产者向其中放数据,消费者从中取数据

static struct put//生产者使用的结构,向其中互斥的放数据
{
    pthread_mutex_t mutex;
    int nput;            //net position to put
    int nval;            //next value to store
} put  = 
{
PTHREAD_MUTEX_INITIALIZER
};

//记录缓冲区的状态,准备好的数目,消费者唯一关注的结构,当然生产者也会使用
static struct nready
{
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int nget;
    int nready;            //number ready for consumer
} nready = 
{
PTHREAD_MUTEX_INITIALIZER,  PTHREAD_COND_INITIALIZER
};//

void *produce(void*);
void *consume(void*);

int main(int argc, char **argv)
{
    if (argc != 4)
    {
        err_quit("Usage: a.out <#items> <#produce_nthreads>
                  <#consume_nthreads>");
    }
    nitems = atoi(argv[1]);
    int produce_nthreads = min(atoi(argv[2]), MAXNTHREADS);
    int consume_nthreads = min(atoi(argv[3]), MAXNTHREADS);

    //Solaris 2.6需要设置线程并发数
    //Set_concurrency(nthreads + 1);

    pthread_t tid_produce[MAXNTHREADS];
    for (int i = 0; i < produce_nthreads; ++i) 
    {
        Pthread_create(&tid_produce[i], NULL, produce, NULL);
    }
    pthread_t tid_consume[MAXNTHREADS];
    for (int i = 0; i < consume_nthreads; ++i)
    {
        Pthread_create(&tid_consume[i], NULL, consume, NULL);
    }

    //等待线程终止
    for (int i = 0; i < produce_nthreads; ++i)
    {
        Pthread_join(tid_produce[i], NULL);
    }
    for (int i = 0; i < consume_nthreads; ++i)
    {
        Pthread_join(tid_consume[i], NULL);
    }

    exit(0);
}

void *produce(void *arg)
{
    printf("producd\n");

    //多个生产者
    for ( ; ; )
    {
        Pthread_mutex_lock(&put.mutex);
        //已存了需要多的数
        if (put.nval >= nitems)
        {
            Pthread_mutex_unlock(&put.mutex);
            return NULL;
        }
        buff[put.nput] = put.nval;
        if (++put.nput >= NBUFF)
        {
            put.nput = 0;
        }
        ++put.nval;
        Pthread_mutex_unlock(&put.mutex);

        //当生产了数据后通知条件变量,应该使临界区尽量短,宁愿使用多个互斥量
        Pthread_mutex_lock(&nready.mutex);
        if (nready.nready == 0)
        {
            Pthread_cond_signal(&nready.cond);
        }
        ++nready.nready;
        Pthread_mutex_unlock(&nready.mutex);
    } //end for(;;)
    return NULL;
}

void *consume(void *argv)
{
    printf("consume\n");

    //多个消费者
    //只生产nitems个选项
    for ( ; ; )
    {
        Pthread_mutex_lock(&nready.mutex);
        //while避免虚假唤醒
        while (nready.nready == 0)
        {
            Pthread_cond_wait(&nready.cond, &nready.mutex);
        }
        //int ival = buff[nready.nget];
        //if (++nready.nget == NBUFF) {
        //    nready.nget = 0;
        //}
        if (++nready.nget >= nitems)
        {
       //nget比较的取值为1..nitems,当为nitems时少操作了一次,总共操作nitems次
            if (nready.nget == nitems)
            {
                printf("buff[%d] = %d\n", nready.nget - 1, 
                        buff[(nready.nget - 1) % NBUFF]);
            }
            Pthread_cond_signal(&nready.cond);
            Pthread_mutex_unlock(&nready.mutex);
            return NULL;
        }
        --nready.nready;
        Pthread_mutex_unlock(&nready.mutex);

        //仅仅读数据不许要互斥
        //if (buff[nready.nget - 1] != nready.nget - 1)
        {
            printf("buff[%d] = %d\n", nready.nget - 1, 
                    buff[(nready.nget - 1) % NBUFF]);
            //printf("buff[%d] = %d\n", nready.nget, 
                      buff[nready.nget]);
       //}
    } //end for(i:0..nitems)
    return NULL;
}

参考资料:
5个经典的同步问题
经典进程同步问题1:生产者-消费者问题
多个生产者——多个消费者模型(互斥量条件变量实现)
生产者消费者模型你知道多少
秒杀多线程第十篇 生产者消费者问题
C++11 并发指南九(综合运用: C++11 多线程下四种生产者消费者模型详解)

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

推荐阅读更多精彩内容

  • 本文摘自汤小丹主编《计算机操作系统》(第三版)2.4 经典进程的同步问 在多道程序环境下,进程同步问题十分重要,也...
    刘帅_阅读 4,608评论 1 1
  • 进程的描述与控制 1.前趋图与程序执行1.1 前趋图介绍:描述程序先后执行顺序,又称为有向无循环图,可记为DAG(...
    孙梦翔阅读 677评论 0 1
  • demo下载[https://github.com/YasinZhou/ThreadLockDemo] 建议一边看...
    Yasin的简书阅读 36,814评论 18 296
  • 有时候,我会想要去走走那一段路。 暮色黄昏,天空的颜色是我迷恋的那一种灰色调。暗暗的好像想要拥抱大地一样低低的压 ...
    情恋风尘阅读 297评论 0 0
  • 有无是非断,乾坤阴阳间。 欲知人生理,心虚求箴言。 岁月有无痕,生命无长欢。 名利如飞雪,大道止于谦。
    管锥一见阅读 240评论 0 7