谈谈我对Linux下“生产者/消费者线程模型”的理解

原创 2016年03月23日 15:39:57

生产者消费者线程模型常常用于网络编程,即一个线程监听事件发生(生产者,例如产生“收到数据”事件),其他线程来处理事件(消费者,例如处理收到的数据)

比较笨的办法是:
消费者线程不停地loop等待数据,当生产者线程发现收到数据时,找一个线程(先不讨论找线程的策略),把“收到数据”这一事件告诉消费者线程。消费者线程会在下一个loop对这个事件进行处理,处理完毕后,继续loop,直到下一个事件到来。

但这么做的缺点显而易见,消费者线程不停地空跑,sleep时间太长,会降低系统的瞬间相应速度;sleep时间太短又会无意义地消耗CPU资源。所以理想中的方法,最好是能有一个事件触发机制,即:消费者线程阻塞等待时间发生,事件一旦触发,立即运行之后的代码,省去了上面方案中等待一个loop的时间,也省去了可能对cpu造成的消耗。

于是,比较好的办法是:
消费者线程阻塞等待事件发生,当生产者线程发现收到数据时,通知某一个消费者线程(同样先不讨论找线程的策略),该消费者线程立即从阻塞中回复,继续执行。

值得庆幸的是,linux提供了API来实现这样的目的:

int pthread_cond_timedwait( pthread_cond_t *restrict cond,
                            pthread_mutex_t *restrict mutex,
                            const struct timespec *restrict abstime);
int pthread_cond_wait(  pthread_cond_t *restrict cond,
                        pthread_mutex_t *restrict mutex); 
//========================= and =================================
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond); 

【★】pthread_cond_wait/pthread_cond_timewait用来等待(两个函数区别在于pthread_cond_timewait会有超时时间,超时会返回,而pthread_cond_wait则会一直阻塞)。
【★】当事件发生时,生产者线程用pthread_cond_signal/pthread_cond_broadcast来激活(两个函数区别在于pthread_cond_signal激活一个线程,pthread_cond_broadcast激活全部线程)。

注意:这里,出现了第一个很容易搞错的问题,即:pthread_cond_signal其实并不一定只激活某一个线程,具体原因在manual中有描述:
这里写图片描述
在多CPU的情况下(多核也是多个CPU),想避免唤醒一个以上的线程是无法做到的。也就是说,即使调用pthread_cond_signal,仍然可能使多个线程的pthread_cond_wait/pthread_cond_timewait返回。而通常情况下的生产者消费者模型中,每一个事件只需要一个消费者线程处理就够了,那么怎么保证一次pthread_cond_signal只唤醒任意一个线程呢,这个随后讨论。

首先,基于上述API的描述,那么直接一点想到的可能是下面的做法:

Consumer Thread:

void ConsumeThread(void* param)
{
    while(true)
    {
        pthread_cond_wait(cond); // 等待条件变量cond激活
        // 消费事件的逻辑
    }
}

Producer Thread:

void ProducerThread(void* param)
{
    while(true)
    {
        // 产生事件的逻辑,如epoll_wait等
        pthread_cond_signal(cond); // 激活等待在cond上的某个线程,让它来处理发生的事件
    }
}

但是,上面的代码有一个问题,pthread_cond_wait却需要一个mutex参数,这个是什么原因呢?这个需要一步步来解释。

首先考虑下面的情况:
因为线程调度的顺序是不可控的,假设某次signal通知消费者线程,有事件发生,在消费者线程执行处理该事件代码时,生产者线程又发送了另一个signal,也就是说,如果pthread_cond_signal在pthread_cond_wait之前执行呢?显然,pthread_cond_wait将错过这次signal的激活
那么,简单地修改可以解决下面的问题,即:增加一个待处理事件列表,根据列表是否为空,来判断是否还有没处理的事件,即不完全依赖signal触发。于是代码变成了下面的样子:
Consumer Thread:

void ConsumeThread(void* param)
{
    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(待处理事件列表.empty() == true) {
            pthread_mutex_unlock(&mutex);
            /* may be race condition part */
            pthread_cond_wait(&cond); // 等待条件变量cond激活
            pthread_mutex_lock(&mutex);
        }
        // 从“待处理事件列表”弹出一个事件;
        pthread_mutex_unlock(&mutex);
        // 消费事件的逻辑
    }
}

Producer Thread:

void ProducerThread(void* param)
{
    while(true)
    {
        // 产生事件的逻辑,如epoll_wait等
        pthread_mutex_lock(&mutex);
        // 待处理事件列表.insert(新事件);
        pthread_cond_signal(cond); // 激活等待在cond上的某个线程,让它来处理发生的事件
        pthread_mutex_unlock(&mutex);
    }
}

这样做,看起来就没什么问题了,我们增加了一个待处理事件列表,在生产者产生事件时,插入到这个列表中,这样即使消费者线程正在干别的,等别的事情干完了,一判断:if(待处理事件列表.empty() == false),就又会接着进入消费的逻辑。从而使事件不会被丢掉。直到真正处理完毕。
虽然看起来一切是美好的,但又不得不考虑这样一个问题:
如果在上述代码ConsumerThread的may be race condition part处产生race condition呢?假设在ConsumerThread执行完unlock后,ProducerThread执行了signal呢?所以,这里就引出了pthread_cond_wait/pthread_cond_timewait为什么需要一个mutex参数的问题。
为了解决上面这个可能出现的race condition,pthread_cond_wait/pthread_cond_timewait在实现时,先进入等待状态,才释放这个mutex,在被激活返回的时候再重新lock,这样就不会存在may be race condition part的空间,也就不会出现漏掉事件的情况。

好,修改一下,代码变成了这样:
Consumer Thread:

void ConsumeThread(void* param)
{
    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(待处理事件列表.empty() == true) {
            pthread_cond_wait(&cond, &mutex); // 等待条件变量cond激活
        }
        // 从“待处理事件列表”弹出一个事件;
        pthread_mutex_unlock(&mutex);
        // 消费事件的逻辑
    }
}

Producer Thread:

void ProducerThread(void* param)
{
    while(true)
    {
        // 产生事件的逻辑,如epoll_wait等
        pthread_mutex_lock(&mutex);
        // 待处理事件列表.insert(新事件);
        pthread_cond_signal(cond); // 激活等待在cond上的某个线程,让它来处理发生的事件
        pthread_mutex_unlock(&mutex);
    }
}

这样一来,总算是没什么race condition问题了,但是,还有一个问题没有解决,也就是一开始提到的:
在多个ConsumerThread的情况下,既然pthread_cond_signal无法保证只使一个线程的pthread_cond_wait/pthread_cond_timewait返回,那怎么保证只有线程去真正的处理事件呢?
终于要引出最终的版本:即陈硕在《linux多线程服务端编程》中提到的,这种模型只有一种正确的实现(只有这一种正确的方法,所以想用错都难),代码如下:

Consumer Thread:

void ConsumeThread(void* param)
{
    while(true)
    {
        pthread_mutex_lock(&mutex);
        // 【注意】这里的if替换成了while
        while(待处理事件列表.empty() == true) {
            pthread_cond_wait(&cond, &mutex); // 等待条件变量cond激活
        }
        // 从“待处理事件列表”弹出一个事件;
        pthread_mutex_unlock(&mutex);
        // 消费事件的逻辑
    }
}

Producer Thread:

void ProducerThread(void* param)
{
    while(true)
    {
        // 产生事件的逻辑,如epoll_wait等
        pthread_mutex_lock(&mutex);
        // 待处理事件列表.insert(新事件);
        pthread_cond_signal(cond); // 激活等待在cond上的某个线程,让它来处理发生的事件
        pthread_mutex_unlock(&mutex);
    }
}

为什么把if替换成while可以解决问题?设想一下,当一个ConsumerThread被唤醒后,这个线程会马上获得mutex锁(回顾一下上面说过的,pthread_cond_wait/pthread_cond_timewait在返回之前会重新对mutex加锁),然后执行while循环的判断,直到把事件弹出,才会释放mutex,这样,等这个线程释放mutex,另一个Consumer再去执行while循环的判断时,已经发现事件被弹出了,没有要处理的了(即使有,也是另外一个事件,不会发生多个ConsumerThread都去拿同一个事件的竞争),然后继续进入等待。以上行为,符合我们对“生产者/消费者模型”的预期。

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Linux线程模型概述

By cszhao1980 1. 轻量进程(LWP) 我们知道进程拥有大量资源,如: (1)寄存器信息,如pc等; (2)Data段 (3)Stack; (4)正文段(可与其他进程共享);...

linux的线程模型:2.6是个分界点

轻量级进程LWP     既然称作轻量级进程,可见其本质仍然是进程,与普通进程相比,LWP与其它进程共享所有(或大部分)逻辑地址空间和系统资源,一个进程可以创建多个LWP,这样它们共享大部分资源...

Linux进程、线程模型,LWP,pthread_self()

一.定义 关于进程、轻量级进程、线程、用户线程、内核线程的定义,这个很容易找到,但是看完之后你可以说你懂了,但实际上你真的明白了么? 在现代操作系统中,进程支持多线程。进程是资源管理的最小单元;而...

mark一个搜索相关技术的博客

http://www.searchtb.com/

Linux 系统应用编程——多线程经典问题(生产者-消费者)

“生产者——消费者”问题是Linux多线程编程中的经典问题,主要是利用信号量处理线程间的同步和互斥问题。          “生产者——消费者”问题描述如下:           有一个有限缓冲区...

http压力测试工具

httpload

生产者消费者问题(Linux多线程下两种实现)

生产者消费者问题是同步问题中的一种常见情况,节

线程同步-生产者消费者问题

在进行多线程编程时,难免还要碰到两个问题,那就线程间的互斥与同步: 线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才...

Linux多线程编程(三)-----生产者与消费者(条件变量,信号量)

Linux多线程编程(一):http://blog.csdn.net/llzk_/article/details/55670172 Linux多线程编程(二):http://blog.csdn.ne...
  • LLZK_
  • LLZK_
  • 2017-02-22 21:02
  • 1185

Linux高级网络开发奇妙之旅

01、Linux网络编程1——网络协议入门 02、Linux网络编程2——无连接和面向连接协议的区别 03、Linux网络编程3——编程准备:字节序、地址转换 04、Linux网络编程4——套接字的介...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)