在 python3 中,新增了内置模块 queue ,模块实现了三种类型的队列,它们的区别仅仅是条目取回的顺序,分别由3个类进行表示,Queue,LifoQueue,PriorityQueue,并且可以很方便地自定义自己的队列并扩展。
queue简介
要理解 python 中的 queue 这种结构,我们就要先理解队列。队列作为一种先入先出的数据结构,通常用来解决有先后关系的数据,或者存在数据间共享变量,需要先后一一处理的情况;
在多线程等并发编程中,线程之间共享数据的访问问题和线程之间的通信问题是要关注的重点。为了解决线程之间的数据共享问题,queue 模块应运而生。 正常请求的多线程,如果是多消费者和多生产者,由于线程是由操作系统调度的,内部的运行时机是不可控的,所以需要手动加锁处理,否则容易出现一些幽灵BUG,非常难排查。
queue 封装了可以用于在多线程并发模式下,安全的访问数据而不会造成数据共享冲突的数据类型,尤其是多生产者、多消费者的场景。
queue 实现了三种子类,分别为:
- Queue(FIFO先入先出型队列)
- LifoQueue(LIFO后入先出型队列)
- PriorityQueue(基于python中的heapq模块实现的堆排序的优先级队列,本篇暂时不介绍具体使用)
queue使用方法
创建一个基础先入先出的队列:
from queue import Queue
q = Queue(maxsize=0)
maxsize 是队列的长度,代表了队列中可以存的数据量上限;一旦达到上限,就不能再插入新的数据,直到队列中的数据被消费掉。如果maxsize小于或者等于0,队列大小没有限制,maxsize 默认值为0。
基本方法
queue 提供了很多基本方法,下面来一一介绍:
Queue.qsize()
:返回队列的大小。
Queue.empty()
:判断队列是否为空,返回 True/False。
Queue.full()
:判断队列是否已满,返回 True/False。
Queue.get(block=True, timeout=None)
:从队列中获取一条数据,block表示在队列空时,是否阻塞等待队列有数据,timeout为阻塞等待时间,默认一直等待。
Queue.getnowait()
:相当于 Queue.get(block=False)
Queue.put(item, block=True, timeout=None)
:往队列中添加一条数据,block表示在队列已满时,是否阻塞等待队列有位置,timeout为阻塞等待时间,默认一直等待。
Queue.putnowait(item)
:相当于 Queue.put(item,block=False)
Queue.join()
:阻塞主线程的运行,直到队列中所有的任务执行完成,才会解开阻塞,继续执行后面的逻辑。
Queue.task_done()
:一般配合 join() 使用,在完成一项工作以后,task_done()告诉队列,该任务已处理完成,join计数-1,当计数为0时,join() 解除阻塞。
简单使用
from queue import Queue
# 队列大小为3
q = Queue(maxsize=3)
## ========= 添加数据 =========
# 添加方式一:超过队列大小,会阻塞在这里
for i in range(4):
q.put(i)
# 添加方式二:超过队列大小,会抛出错误 queue.Full
for i in range(4):
q.put_nowait(i)
## ========= 获取数据 =========
# 先插入3条数据
for i in range(3):
q.put(i)
# 获取方式一: 超过队列中的数据数,会阻塞在这里
for i in range(4):
q.get()
# 获取方式二: 超过队列中的数据数,会抛出错误 _queue.Empty
for i in range(4):
q.get_nowait()
源码分析
queue 模块的源码并不多,但是设计思想和原则还是非常受用的,我们可以深入学习一下其中的思想,先从初始化函数开始。
初始化函数
def __init__(self, maxsize=0):
# 设置队列大小
self.maxsize = maxsize
# 设置底层数据结构
self._init(maxsize)
# 初始化锁,保证线程安全,互斥锁
self.mutex = threading.Lock()
self.not_empty = threading.Condition(self.mutex)
self.not_full = threading.Condition(self.mutex)
self.all_tasks_done = threading.Condition(self.mutex)
# 记录队列中未完成的任务数量
self.unfinished_tasks = 0
def _init(self, maxsize):
# 初始化底层数据结构,可替换为自己的数据结构
self.queue = deque()
初始化函数主要做了几件事情:
- 设置队列大小。
- 设置队列底层对应的数据结构。
- 设置了一把内部的全局锁,主要用来控制多线程时的粒度,保证多线程的信息安全。
- 设置了多种锁对应的情况,分别是
not_empty
队列不空时,not_full
队列不满时,all_tasks_done
任务全部完成时,用来控制队列处于不同状态时,是否允许多线程共同访问数据等情况。
状态判断
基于上面的信息,我们可以知道队列会对应很多种状态,会有一些判断队列状态和情况的方法。
def qsize(self):
# 返回队列中目前的数据大小
with self.mutex:
return self._qsize()
def empty(self):
# 队列是否为空
with self.mutex:
return not self._qsize()
def full(self):
# 队列是否满了
with self.mutex:
return 0 < self.maxsize <= self._qsize()
def _qsize(self):
return len(self.queue)
上面的方法都非常简单清晰,可以看到,每次判断时都上了锁,这就保证了多线程访问的安全性。
入队操作
对于一个队列来说,最重要的就是入队、出队的操作了,先介绍入队,主要就是一个 put 方法。
def put(self, item, block=True, timeout=None):
with self.not_full:
if self.maxsize > 0:
if not block:
if self._qsize() >= self.maxsize:
raise Full
elif timeout is None:
while self._qsize() >= self.maxsize:
self.not_full.wait()
elif timeout < 0:
raise ValueError("'timeout' must be a non-negative number")
else:
endtime = time() + timeout
while self._qsize() >= self.maxsize:
remaining = endtime - time()
if remaining <= 0.0:
raise Full
self.not_full.wait(remaining)
self._put(item)
self.unfinished_tasks += 1
self.not_empty.notify()
def put_nowait(self, item):
return self.put(item, block=False)
put 方法虽然代码量不大,但是却处理了很多信息,也是核心方法,主要的逻辑如下:
在拿到锁的情况下,判断maxsize
是否大于0,如果是无限大小的队列,则直接往队列中添加一个任务,并将任务计数+1,同时通知 not_empty ,唤醒在其中等待数据的线程。
maxsize
大于0,则为有界队列,有以下几种情况:
1.block 为 False,忽略timeout参数。
若此时队列已满,则抛出 Full 异常;
若此时队列未满,则添加数据到队列中;
2.block 为 True。
若 timeout 是 None 时,那么put操作可能会阻塞,直到队列中有空闲的空间;
若 timeout 是非负数,则会阻塞相应时间直到队列中有剩余空间,如果超过这个时间队列仍然没空间,抛出 Full 异常;
put_nowait 就是非阻塞的调用 put 方法,非常简单,不再赘述。
出队操作
出队的核心方法如下:
def get(self, block=True, timeout=None):
with self.not_empty:
if not block:
if not self._qsize():
raise Empty
elif timeout is None:
while not self._qsize():
self.not_empty.wait()
elif timeout < 0:
raise ValueError("'timeout' must be a non-negative number")
else:
endtime = time() + timeout
while not self._qsize():
remaining = endtime - time()
if remaining <= 0.0:
raise Empty
self.not_empty.wait(remaining)
item = self._get()
self.not_full.notify()
return item
def get_nowait(self):
return self.get(block=False)
get 与 put 操作正好相反,主要逻辑也是与block
和timeout
参数相关。
1.如果 block 为 False,忽略timeout参数。
若此时队列没有元素,则抛出 Empty 异常;
若此时队列有元素,则取出一个元素并赋值给 item 返回;
2.如果 block 为 True。
若 timeout 是 None 时,那么get操作可能会阻塞,直到队列中有元素;
若 timeout 是非负数,则会阻塞相应时间直到队列中有元素,如果超过这个时间队列中仍然没有元素,则抛出 Empty 异常;
在取出元素后,就会通知正在等待 put 数据的线程,如果有阻塞的,就可以继续添加数据了。
完成任务和逻辑阻塞
在介绍完入队、出队的操作后,整个队列的脉络就大致清晰了,目前还有两个比较重要的方法没有介绍,主要是与队列中任务的完成和主线程逻辑的阻塞相关,我们可能会遇到下面这样的场景:多线程执行完所有的方法后,主线程的逻辑才能继续往下执行。
针对这种场景,Queue 提供了很好的支持,主要是task_done
和join
两个方法:
def task_done(self):
with self.all_tasks_done:
unfinished = self.unfinished_tasks - 1
if unfinished <= 0:
if unfinished < 0:
raise ValueError('task_done() called too many times')
self.all_tasks_done.notify_all()
self.unfinished_tasks = unfinished
def join(self):
with self.all_tasks_done:
while self.unfinished_tasks:
self.all_tasks_done.wait()
task_done
方法主要用于在线程完成任务的处理后通知队列,并将队列的未完成任务数-1。在锁的控制下,保证线程安全。当调用次数大于 put 放入队列的数量时,会抛出 ValueError。
join
方法会判断是否有未完成的任务数,大于0时,join 就会一直阻塞在这里,无法继续执行下面的逻辑。
task_done
和join
必须配合使用,才能达到阻塞的效果。
自定义队列
我们上面主要介绍的是 Queue ,默认使用的是FIFO先入先出型队列。当我们要使用LIFO后入先出型队列时,通过看源码,我们发现,LifoQueue 就是继承了 Queue 对象,并重写了下面几个方法:
class LifoQueue(Queue):
def _init(self, maxsize):
self.queue = []
def _qsize(self):
return len(self.queue)
def _put(self, item):
self.queue.append(item)
def _get(self):
return self.queue.pop()
这里的设计非常巧妙,当我们想实现一个自己的队列时,线程安全的问题已经被 Queue本身处理了,我们只需要在_init
中定义自己的数据结构,并且遵循下面的规则即可实现一个自己的队列:
_qsize
:返回队列的大小。_put
:实现放入队列的操作。_get
:实现获取队列数据的操作。