用Python的线程来解决生产者消费问题的示例


Posted in Python onApril 02, 2015

我们将使用Python线程来解决Python中的生产者—消费者问题。这个问题完全不像他们在学校中说的那么难。

如果你对生产者—消费者问题有了解,看这篇博客会更有意义。

为什么要关心生产者—消费者问题:

  •     可以帮你更好地理解并发和不同概念的并发。
  •     信息队列中的实现中,一定程度上使用了生产者—消费者问题的概念,而你某些时候必然会用到消息队列。

当我们在使用线程时,你可以学习以下的线程概念:

  •     Condition:线程中的条件。
  •     wait():在条件实例中可用的wait()。
  •     notify() :在条件实例中可用的notify()。

我假设你已经有这些基本概念:线程、竞态条件,以及如何解决静态条件(例如使用lock)。否则的话,你建议你去看我上一篇文章basics of Threads。

引用维基百科:

生产者的工作是产生一块数据,放到buffer中,如此循环。与此同时,消费者在消耗这些数据(例如从buffer中把它们移除),每次一块。

这里的关键词是“同时”。所以生产者和消费者是并发运行的,我们需要对生产者和消费者做线程分离。
 

from threading import Thread
 
class ProducerThread(Thread):
  def run(self):
    pass
 
class ConsumerThread(Thread):
  def run(self):
    pass

再次引用维基百科:

这个为描述了两个共享固定大小缓冲队列的进程,即生产者和消费者。

假设我们有一个全局变量,可以被生产者和消费者线程修改。生产者产生数据并把它加入到队列。消费者消耗这些数据(例如把它移出)。

queue = []

在刚开始,我们不会设置固定大小的条件,而在实际运行时加入(指下述例子)。

一开始带bug的程序:

from threading import Thread, Lock
import time
import random
 
queue = []
lock = Lock()
 
class ProducerThread(Thread):
  def run(self):
    nums = range(5) #Will create the list [0, 1, 2, 3, 4]
    global queue
    while True:
      num = random.choice(nums) #Selects a random number from list [0, 1, 2, 3, 4]
      lock.acquire()
      queue.append(num)
      print "Produced", num
      lock.release()
      time.sleep(random.random())
 
class ConsumerThread(Thread):
  def run(self):
    global queue
    while True:
      lock.acquire()
      if not queue:
        print "Nothing in queue, but consumer will try to consume"
      num = queue.pop(0)
      print "Consumed", num
      lock.release()
      time.sleep(random.random())
 
ProducerThread().start()
ConsumerThread().start()

运行几次并留意一下结果。如果程序在IndexError异常后并没有自动结束,用Ctrl+Z结束运行。

样例输出:
 

Produced 3
Consumed 3
Produced 4
Consumed 4
Produced 1
Consumed 1
Nothing in queue, but consumer will try to consume
Exception in thread Thread-2:
Traceback (most recent call last):
 File "/usr/lib/python2.7/threading.py", line 551, in __bootstrap_inner
  self.run()
 File "producer_consumer.py", line 31, in run
  num = queue.pop(0)
IndexError: pop from empty list

解释:

  •     我们开始了一个生产者线程(下称生产者)和一个消费者线程(下称消费者)。
  •     生产者不停地添加(数据)到队列,而消费者不停地消耗。
  •     由于队列是一个共享变量,我们把它放到lock程序块内,以防发生竞态条件。
  •     在某一时间点,消费者把所有东西消耗完毕而生产者还在挂起(sleep)。消费者尝试继续进行消耗,但此时队列为空,出现IndexError异常。
  •     在每次运行过程中,在发生IndexError异常之前,你会看到print语句输出”Nothing in queue, but consumer will try to consume”,这是你出错的原因。

我们把这个实现作为错误行为(wrong behavior)。

什么是正确行为?

当队列中没有任何数据的时候,消费者应该停止运行并等待(wait),而不是继续尝试进行消耗。而当生产者在队列中加入数据之后,应该有一个渠道去告诉(notify)消费者。然后消费者可以再次从队列中进行消耗,而IndexError不再出现。

关于条件

    条件(condition)可以让一个或多个线程进入wait,直到被其他线程notify。参考:?http://docs.python.org/2/library/threading.html#condition-objects

这就是我们所需要的。我们希望消费者在队列为空的时候wait,只有在被生产者notify后恢复。生产者只有在往队列中加入数据后进行notify。因此在生产者notify后,可以确保队列非空,因此消费者消费时不会出现异常。

  •     condition内含lock。
  •     condition有acquire()和release()方法,用以调用内部的lock的对应方法。

condition的acquire()和release()方法内部调用了lock的acquire()和release()。所以我们可以用condiction实例取代lock实例,但lock的行为不会改变。
生产者和消费者需要使用同一个condition实例, 保证wait和notify正常工作。

重写消费者代码:
 

from threading import Condition
 
condition = Condition()
 
class ConsumerThread(Thread):
  def run(self):
    global queue
    while True:
      condition.acquire()
      if not queue:
        print "Nothing in queue, consumer is waiting"
        condition.wait()
        print "Producer added something to queue and notified the consumer"
      num = queue.pop(0)
      print "Consumed", num
      condition.release()
      time.sleep(random.random())

重写生产者代码:
 

class ProducerThread(Thread):
  def run(self):
    nums = range(5)
    global queue
    while True:
      condition.acquire()
      num = random.choice(nums)
      queue.append(num)
      print "Produced", num
      condition.notify()
      condition.release()
      time.sleep(random.random())

样例输出:
 

Produced 3
Consumed 3
Produced 1
Consumed 1
Produced 4
Consumed 4
Produced 3
Consumed 3
Nothing in queue, consumer is waiting
Produced 2
Producer added something to queue and notified the consumer
Consumed 2
Nothing in queue, consumer is waiting
Produced 2
Producer added something to queue and notified the consumer
Consumed 2
Nothing in queue, consumer is waiting
Produced 3
Producer added something to queue and notified the consumer
Consumed 3
Produced 4
Consumed 4
Produced 1
Consumed 1

解释:

  •     对于消费者,在消费前检查队列是否为空。
  •     如果为空,调用condition实例的wait()方法。
  •     消费者进入wait(),同时释放所持有的lock。
  •     除非被notify,否则它不会运行。
  •     生产者可以acquire这个lock,因为它已经被消费者release。
  •     当调用了condition的notify()方法后,消费者被唤醒,但唤醒不意味着它可以开始运行。
  •     notify()并不释放lock,调用notify()后,lock依然被生产者所持有。
  •     生产者通过condition.release()显式释放lock。
  •     消费者再次开始运行,现在它可以得到队列中的数据而不会出现IndexError异常。

为队列增加大小限制

生产者不能向一个满队列继续加入数据。

它可以用以下方式来实现:

  •     在加入数据前,生产者检查队列是否为满。
  •     如果不为满,生产者可以继续正常流程。
  •     如果为满,生产者必须等待,调用condition实例的wait()。
  •     消费者可以运行。消费者消耗队列,并产生一个空余位置。
  •     然后消费者notify生产者。
  •     当消费者释放lock,消费者可以acquire这个lock然后往队列中加入数据。

最终程序如下:

from threading import Thread, Condition
import time
import random
 
queue = []
MAX_NUM = 10
condition = Condition()
 
class ProducerThread(Thread):
  def run(self):
    nums = range(5)
    global queue
    while True:
      condition.acquire()
      if len(queue) == MAX_NUM:
        print "Queue full, producer is waiting"
        condition.wait()
        print "Space in queue, Consumer notified the producer"
      num = random.choice(nums)
      queue.append(num)
      print "Produced", num
      condition.notify()
      condition.release()
      time.sleep(random.random())
 
class ConsumerThread(Thread):
  def run(self):
    global queue
    while True:
      condition.acquire()
      if not queue:
        print "Nothing in queue, consumer is waiting"
        condition.wait()
        print "Producer added something to queue and notified the consumer"
      num = queue.pop(0)
      print "Consumed", num
      condition.notify()
      condition.release()
      time.sleep(random.random())
 
ProducerThread().start()
ConsumerThread().start()

样例输出:
 

Produced 0
Consumed 0
Produced 0
Produced 4
Consumed 0
Consumed 4
Nothing in queue, consumer is waiting
Produced 4
Producer added something to queue and notified the consumer
Consumed 4
Produced 3
Produced 2
Consumed 3

更新:
很多网友建议我在lock和condition下使用Queue来代替使用list。我同意这种做法,但我的目的是展示Condition,wait()和notify()如何工作,所以使用了list。

以下用Queue来更新一下代码。

Queue封装了Condition的行为,如wait(),notify(),acquire()。

现在不失为一个好机会读一下Queue的文档(http://docs.python.org/2/library/queue.html)。

更新程序:

from threading import Thread
import time
import random
from Queue import Queue
 
queue = Queue(10)
 
class ProducerThread(Thread):
  def run(self):
    nums = range(5)
    global queue
    while True:
      num = random.choice(nums)
      queue.put(num)
      print "Produced", num
      time.sleep(random.random())
 
class ConsumerThread(Thread):
  def run(self):
    global queue
    while True:
      num = queue.get()
      queue.task_done()
      print "Consumed", num
      time.sleep(random.random())
 
ProducerThread().start()
ConsumerThread().start()

解释:

  •     在原来使用list的位置,改为使用Queue实例(下称队列)。
  •     这个队列有一个condition,它有自己的lock。如果你使用Queue,你不需要为condition和lock而烦恼。
  •     生产者调用队列的put方法来插入数据。
  •     put()在插入数据前有一个获取lock的逻辑。
  •     同时,put()也会检查队列是否已满。如果已满,它会在内部调用wait(),生产者开始等待。
  •     消费者使用get方法。
  •     get()从队列中移出数据前会获取lock。
  •     get()会检查队列是否为空,如果为空,消费者进入等待状态。
  •     get()和put()都有适当的notify()。现在就去看Queue的源码吧。
Python 相关文章推荐
Python中特殊函数集锦
Jul 27 Python
Python利用ElementTree模块处理XML的方法详解
Aug 31 Python
Windows下Anaconda的安装和简单使用方法
Jan 04 Python
Python决策树之基于信息增益的特征选择示例
Jun 25 Python
python编程使用协程并发的优缺点
Sep 20 Python
Linux 修改Python命令的方法示例
Dec 03 Python
Django框架中间件定义与使用方法案例分析
Nov 28 Python
python关于变量名的基础知识点
Mar 03 Python
jupyter修改文件名方式(TensorFlow)
Apr 21 Python
Pytorch 使用CNN图像分类的实现
Jun 16 Python
python 无损批量压缩图片(支持保留图片信息)的示例
Sep 22 Python
python实现图书馆抢座(自动预约)功能的示例代码
Sep 29 Python
用实例分析Python中method的参数传递过程
Apr 02 #Python
使用优化器来提升Python程序的执行效率的教程
Apr 02 #Python
使用Python脚本对Linux服务器进行监控的教程
Apr 02 #Python
在Python编程过程中用单元测试法调试代码的介绍
Apr 02 #Python
用Python的Django框架完成视频处理任务的教程
Apr 02 #Python
用map函数来完成Python并行任务的简单示例
Apr 02 #Python
对于Python异常处理慎用“except:pass”建议
Apr 02 #Python
You might like
PHP实现用户认证及管理完全源码
2007/03/11 PHP
php获取文件内容最后一行示例
2014/01/09 PHP
php准确计算复活节日期的方法
2015/04/18 PHP
php+html5基于websocket实现聊天室的方法
2015/07/17 PHP
PHP curl模拟登录带验证码的网站
2015/11/30 PHP
DWZ+ThinkPHP开发时遇到的问题分析
2016/12/12 PHP
thinkPHP框架实现的短信接口验证码功能示例
2018/06/20 PHP
JavaScript delete 属性的使用
2009/10/08 Javascript
Jquery 动态添加按钮实现代码
2010/05/06 Javascript
获得所有表单值的JQuery实现代码[IE暂不支持]
2012/05/24 Javascript
js函数setTimeout延迟执行的简单介绍
2013/07/17 Javascript
jquery制作select列表双向选择示例代码
2014/09/02 Javascript
JavaScript fontcolor方法入门实例(按照指定的颜色来显示字符串)
2014/10/17 Javascript
AngularJS学习笔记之ng-options指令
2015/06/16 Javascript
js采用concat和sort将N个数组拼接起来的方法
2016/01/21 Javascript
jquery uploadify如何取消已上传成功文件
2017/02/08 Javascript
AngularJS动态绑定ng-options的ng-model实例代码
2017/06/21 Javascript
基于require.js的使用(实例讲解)
2017/09/07 Javascript
详解组件库的webpack构建速度优化
2018/06/18 Javascript
ligerUI的ligerDialog关闭刷新的方法
2019/09/27 Javascript
JavaScript实现打砖块游戏
2020/02/25 Javascript
解决vue单页面多个组件嵌套监听浏览器窗口变化问题
2020/07/30 Javascript
Python玩转加密的技巧【推荐】
2019/05/13 Python
Python3日期与时间戳转换的几种方法详解
2019/06/04 Python
wxpython绘制音频效果
2019/11/18 Python
Python3爬虫mitmproxy的安装步骤
2020/07/29 Python
python两个list[]相加的实现方法
2020/09/23 Python
Python+unittest+requests+excel实现接口自动化测试框架
2020/12/23 Python
线程同步的方法
2016/11/23 面试题
公司委托书格式范文
2014/04/04 职场文书
公司经理任命书
2014/06/05 职场文书
严以用权学习心得体会
2016/01/12 职场文书
Mysql服务添加 iptables防火墙策略的方案
2021/04/29 MySQL
详解vue中v-for的key唯一性
2021/05/15 Vue.js
Hive导入csv文件示例
2022/06/25 数据库
MyBatis在注解上使用动态SQL方式(@select使用if)
2022/07/07 Java/Android