用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中用于返回绝对值的abs()方法
May 14 Python
python使用pymysql实现操作mysql
Sep 13 Python
Python 异常处理的实例详解
Sep 11 Python
Python语言描述机器学习之Logistic回归算法
Dec 21 Python
python实现kmp算法的实例代码
Apr 03 Python
使用python进行波形及频谱绘制的方法
Jun 17 Python
在PyCharm的 Terminal(终端)切换Python版本的方法
Aug 02 Python
python定时任务 sched模块用法实例
Nov 04 Python
Python统计文本词汇出现次数的实例代码
Feb 27 Python
Python Selenium安装及环境配置的实现
Mar 17 Python
对django 2.x版本中models.ForeignKey()外键说明介绍
Mar 30 Python
Python读取ini配置文件传参的简单示例
Jan 05 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
全文搜索和替换
2006/10/09 PHP
用PHP制作的意见反馈表源码
2007/03/11 PHP
PHP5与MySQL数据库操作常用代码 收集
2010/03/21 PHP
PHP基于单例模式实现的数据库操作基类
2016/01/15 PHP
基于php判断客户端类型
2016/10/14 PHP
PHP实现二维数组根据key进行排序的方法
2016/12/30 PHP
谈谈从phpinfo中能获取哪些值得注意的信息
2017/03/28 PHP
浅谈PHP匿名函数和闭包
2019/03/08 PHP
Discuz不使用插件实现简单的打赏功能
2019/03/21 PHP
Javascript学习笔记8 用JSON做原型
2010/01/11 Javascript
一个网马的tips实现分析
2010/11/28 Javascript
ASP.NET中AJAX 调用实例代码
2012/05/03 Javascript
node.js中Socket.IO的进阶使用技巧
2014/11/04 Javascript
JavaScript Length 属性的总结
2015/11/02 Javascript
用canvas 实现个图片三角化(LOW POLY)效果
2016/02/18 Javascript
Node.js利用断言模块assert进行单元测试的方法
2017/09/28 Javascript
Vue-resource拦截器判断token失效跳转的实例
2017/10/27 Javascript
ES6 Generator函数的应用实例分析
2019/06/26 Javascript
jQuery操作元素追加内容示例
2020/01/10 jQuery
解决VUEX的mapState/...mapState等取值问题
2020/07/24 Javascript
微信小程序tab左右滑动切换功能的实现代码
2021/02/08 Javascript
Django中URL视图函数的一些高级概念介绍
2015/07/20 Python
对python 各种删除文件失败的处理方式分享
2018/04/24 Python
分享vim python缩进等一些配置
2018/07/02 Python
Python with用法:自动关闭文件进程
2019/07/10 Python
Rag & Bone官网:瑞格布恩高级成衣
2018/04/19 全球购物
Java里面如何把一个Array数组转换成Collection, List
2013/07/26 面试题
手术室护士自我鉴定
2013/10/14 职场文书
幼儿园新年寄语
2014/04/03 职场文书
小学生评语集锦
2014/04/18 职场文书
车贷收入证明范本
2014/09/14 职场文书
2014年爱国卫生工作总结
2014/11/22 职场文书
荒岛余生观后感
2015/06/09 职场文书
2016年小学生新年寄语
2015/08/18 职场文书
离婚协议书格式范本
2016/03/18 职场文书
Python基础之数据结构详解
2021/04/28 Python