Python中的生产者-消费者问题
我们将使用 Python 线程解决 Python 中的生产者消费者问题
为什么要关心生产者消费者问题:
- 将帮助您更多地了解并发和不同的并发概念
- 生产者消费者问题的概念在一定程度上用于实现消息队列。而且您肯定会在某个时间点需要消息队列
在我们使用线程时,您将了解以下线程主题:
- 线程中的条件
- wait()方法可用于 Condition 实例
- notify()方法可用于 Condition 实例
引用维基百科:
The producer's job is to generate a piece of data, put it into the buffer and start again.
At the same time, the consumer is consuming the data (i.e., removing it from the buffer) one piece at a time
生产者的工作是生成一段数据,将其放入缓冲区并重新开始
与此同时,使用者一次只消耗一部分数据(即从缓冲区中删除数据)
这里的关键是“同时”。因此,生产者和消费者需要同时运行。因此,我们需要为生产者和消费者提供单独的线程
from threading import Thread
class ProducerThread(Thread):
def run(self):
pass
class ConsumerThread(Thread):
def run(self):
pass
再次引用维基百科:
The problem describes two processes, the producer and the consumer, who share a common,
fixed-size buffer used as a queue.
这个问题描述了两个过程,生产者和消费者,它们共享一个固定大小的缓冲区用作队列
因此,我们保留一个变量,该变量将是全局的,并且将被生产者和消费者线程修改。生产者生成数据并将其添加到队列中。消费者从队列中消费数据,即将它从队列中移除。
queue = []
在第一次迭代中,我们不会对队列施加固定大小的约束。一旦我们的基本程序工作,我们将使其固定大小。
初始错误程序:
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
解释:
- 我们启动了一个生产者线程(以下简称生产者)和一个消费者线程(以下简称消费者)。
- 生产者不断添加到队列中,消费者不断从队列中移除。
- 由于队列是一个共享变量,我们将它保存在锁内以避免竞争条件。
- 在某些时候,消费者已经消费了所有东西,而生产者仍在睡觉。消费者尝试消费更多,但由于队列为空,因此引发了IndexError。
- 但是在每次执行时,在引发 IndexError 之前,您会看到打印语句告诉“队列中没有任何内容,但消费者将尝试消费”,这解释了您收到错误的原因。
我们发现这种实现是错误的行为
正确的行为是什么?
当队列中没有任何内容时,消费者应该停止运行并等待,而不是尝试从队列中消费。一旦生产者向队列中添加了一些东西,它应该有一种方法可以通知消费者告诉它已经向队列中添加了一些东西。因此,消费者可以再次从队列中消费。因此永远不会引发 IndexError
关于Condition
- Condition对象允许一个或多个线程等待,直到另一个线程通知。取自这里。
这正是我们想要的。我们希望消费者在队列为空时等待,并仅在生产者通知时恢复。生产者应仅在将某些内容添加到队列后才通知。因此,在生产者通知后,我们可以确定队列不为空,因此如果消费者消费,则不会出现错误。
- Condition总是与锁相关联。
- Condition具有调用关联锁的相应方法的acquire() 和release() 方法
Condition提供了acquire()和release(),它们在内部调用锁的acquire()和release(),所以我们可以用condition实例替换lock实例,我们的锁行为会继续正常工作。
消费者需要使用condition实例等待,生产者也需要使用condition实例通知消费者。因此,他们必须使用相同的condition实例,等待和通知功能才能正常工作。
让我们重写我们的消费者和生产者代码:
Producer 代码:
from threading import Condition
condition = Condition()
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())
Consumer 代码:
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())
样本输出:
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
解释:
- 对于消费者,我们在消费前检查队列是否为空
- 如果是,则在条件实例上调用wait()
- wait() 阻塞消费者并释放与条件关联的锁。这个锁是由消费者持有的,所以基本上消费者失去了对锁的持有
- 现在除非通知消费者,否则它不会运行
- 生产者可以获取锁,因为锁已被消费者释放
- 生产者将数据放入队列并在条件实例上调用 notify()
- 一旦根据条件进行 notify() 调用,消费者就会醒来。但醒来并不意味着它开始执行
- notify() 不会释放锁。即使在 notify() 之后,锁仍然由生产者持有
- Producer 使用 condition.release() 显式释放锁
- 消费者再次开始运行。现在它将在队列中找到数据,并且不会引发 IndexError
在队列中添加最大大小
如果队列已满,生产者不应将数据放入队列中。
可以通过以下方式完成:
- 在将数据放入队列之前,生产者应该检查队列是否已满
- 如果没有,生产者可以照常继续
- 如果队列已满,生产者必须等待。因此,在条件实例上调用wait()来完成此操作
- 这给了消费者一个运行的机会。消费者将消费队列中的数据,这将在队列中创建空间
- 然后消费者应该通知生产者
- 一旦消费者释放锁,生产者就可以获取锁并将数据添加到队列中
最终程序如下所示:
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
更新:
Queue封装了Condition、wait()、notify()、acquire()等的行为
更新程序:
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()
解释
- 代替列表,我们使用队列实例(以下简称队列)
- 队列有一个条件并且该条件有它的锁。如果您使用 Queue,则无需担心 Condition 和 Lock
- Producer 使用put available on queue 在队列中插入数据
- put() 具有在将数据插入队列之前获取锁的逻辑
- put() 还会检查队列是否已满。如果是,则它在内部调用wait(),因此生产者开始等待
- 消费者使用get
- get() 在从队列中删除数据之前获取锁
- get() 检查队列是否为空。如果是,它将消费者置于等待状态
- get() 和 put() 也有适当的 notify() 逻辑
- 为什么不现在检查 Queue 的源代码?