队列是一种常见的线性表数据结构,队列通常应用在资源有限的场景下,比如线程池、数据库连接池等。当线程池没有空闲线程时,新的任务请求线程资源时,将任务排队,等到有空闲线程时,取出排队的任务继续处理。
26.1 队列基本操作
它的典型特征是先进先出。即先入队列的先出队,后入队列的后出队。队列有两个基本操作:入队(enqueue),即将一个数据放入到队列的尾部,出队(dequeque),即将一个数据从队头移除。下图是一个单向队列的操作示意图:
26.2 有界队列与无界队列
队列根据其中容纳的数据个数,可以分为无界队列(unbounded queue)和有界队列(bounded queue)。无界队列中可以存放任意多个数据,有界队列中可以放得数据个数是有限的。队列大小要设置的合理,队列太大导致等待的任务太多,队列太小会导致无法充分利用系统资源。
26.3 Python提供的队列
Python提供了队列相关模块queue,这个模块提供了三种常用的队列,分别是:
- 先进先出(FIFO)队列Queue,普通队列,即先插入的先被删除。
- 优先级队列PriorityQueue,根据优先级顺序来决定出队列的顺序。
- 后进先出(LIFO)队列LifoQueue,类似于栈,最近添加的条目是最先被检索到。
另外,Python的collections模块提供了双端队列deque,可以在队列两端进行入队和出队,既可以作为队列来使用,也可以作为栈来使用。
26.3.1 先进先出(FIFO)队列queue.Queue
这是Python中最基础的一个队列。在Pycharm中按两下shift,搜索框中输入queue后,按回车键进入queue.py文件,在Pycharm上点击Navigate——File Structure。可以看到queue模块的文件结构。LifoQueue和PriorityQueue都是继承自Queue的。
上图中列出来Queue这个类中所有的方法,先来看下其中几个常用的方法:
import queue
q = queue.Queue() # 没有传入maxsize,表示这是个无界队列。
for i in range(5):
q.put(i) # 放入元素到队列,在队列满时会阻塞
while not q.empty(): # 不为空
print(q.get(), end=" ") # 从队列中取元素,在队列为空时会阻塞
除了上面阻塞操作的入队和出队,还有两个非阻塞的入队和出队方法:
- get_nowait() ,在队列为空的时候也不阻塞,这时候会抛异常queue.Empty
- put_nowait(1) ,在队列满的时候也不阻塞,这时候会抛异常queue.Full
import queue
q = queue.Queue(5) # 传入maxsize=5,表示这队列长度为5。
for i in range(5):
try:
q.put_nowait(i) # 放入元素到队列,在队列满时会阻塞
except queue.Full as e:
print("队列满了")
while True:
try:
print(q.get_nowait()) # 在队列为空的时候也不阻塞,这时候会抛异常queue.Empty
except queue.Empty:
print('队列为空')
break
这个队列最常用在生产者-消费者模型上。在这个模型中,生产者往队列中放入数据,消费者从队列中读取数据。这个模型实现了系统之间的解耦,生产者和消费者各自独立的系统,通过队列来传递数据,各自可以水平扩展和优化,彼此不影响。
下面是一个完整生产者和消费者的例子(原创):
import concurrent.futures
import os
import queue
import random
import threading
import time
# 生产者,定义生产逻辑,将生产的数据放入Queue中
def produce(q):
production = '{}-{}'.format(threading.current_thread().name, int(time.time()))
time.sleep(random.randint(1, 2))
production_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print('时间:%s, %s 生产了%s, 库存是%d' % (production_date, threading.current_thread().name, production, q.qsize()))
q.put(production)
# 消费者,定义消费数据的逻辑,从队列中取数据
def consume(q):
production = q.get()
time.sleep(random.randint(1, 2))
consume_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print('时间:%s, %s 购买了%s, 库存是%d' % (consume_date, threading.current_thread().name, production, q.qsize()))
if __name__ == '__main__':
# 生产者线程池,实现多个生产者同时生产
def produce_pool(workers, q):
with concurrent.futures.ThreadPoolExecutor(max_workers=workers,
thread_name_prefix='producer') as executor:
while True:
executor.submit(produce, q)
# 消费者线程池,实现多个消费者同时消费
def consume_pool(workers, q):
with concurrent.futures.ThreadPoolExecutor(max_workers=workers,
thread_name_prefix='consumer') as executor:
while True:
executor.submit(consume, q)
qq = queue.Queue(20)
produce_workers = os.cpu_count() # 生产者线程数量
consume_workers = os.cpu_count() # 消费者线程数量
producer_thread = threading.Thread(target=produce_pool, args=(produce_workers, qq))
consumer_thread = threading.Thread(target=consume_pool, args=(consume_workers, qq))
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()
这个例子中,生产者和消费者都有各自的线程池,可以通过调整自己线程池的数量来实现各自能力的提升,比如如果发现队列中数据长时间达到队列最大值,说明消费者消费的慢了,或者消费者线程数量太少了,就可以扩大comsume_workers的值,提高消费能力。如果发现队列中数据长时间是空,说明生产者生产的满了,或者生产者线程数太少了,可以扩大produce_workder的值,提供生产能力。
上面的例子为生产者线程池和消费者线程池又各自创建立独立的线程,运行代码后将看到多个生产者和消费者在同时进行工作。
时间:2020-06-07 12:26:28, producer_0 生产了producer_0-1591503986, 库存是2
时间:2020-06-07 12:26:28, producer_3 生产了producer_3-1591503986, 库存是3
时间:2020-06-07 12:26:28, consumer_1 购买了producer_2-1591503984, 库存是4
时间:2020-06-07 12:26:28, consumer_3 购买了producer_5-1591503984, 库存是2
时间:2020-06-07 12:26:29, consumer_7 购买了producer_0-1591503984, 库存是1
上面这个例子中的生产者消费者,只是在Console中打印了一些内容。在实际的工作中,消费者往往是将处理后的数据return给调用者的。下面就来改造一下,如果消费者有返回值该如何处理。
首先,comsume函数添加一个return语句,返回对商品production的评价。然后,在消费者线程池函数comsume_pool中,通过future实例的result方法获取consume函数的执行结果。
def consume(q): # 消费者改进版,对消费的产品给出评价
production = q.get()
return "{} 很棒~,好评".format(production)
def consume_pool(workers, q):
with concurrent.futures.ThreadPoolExecutor(max_workers=workers,
thread_name_prefix='consumer') as executor:
to_do = [] # 存放future实例
results = [] # 存储future实例的结果
while True:
to_do.append(executor.submit(consume, q)) # submit返回创建好的 future 实例,放入to_do列表
if len(to_do) >= 5:
for done_future in concurrent.futures.as_completed(to_do): # 返回任务的对象,这里不耗时
results.append(done_future.result()) # 耗时,注意取得的结果是乱序的
print(results)
to_do = []
26.3.2 后进先出(LIFO)队列queue.LifoQueue
这是一个类似与栈的数据结构,后进队列的数据先出来。通过Pycharm的Navigate——Type Hierachy可以看到LifoQueue继承自Queue。
通过查看LifoQueue的源码发现,它本身并没有特殊的方法。对它的操作依然是使用它的父类Queue的方法,只不过对于get方法,这里定义了自己的逻辑,是从队尾获取数据。
class LifoQueue(Queue):
'''Variant of Queue that retrieves most recently added entries first.'''
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的源码可以看到get方法获取数据是从队首:
# Get an item from the queue
def _get(self):
return self.queue.popleft()
这也就是LifoQueue相比Queue的全部区别了。举个例子感受一下:
import queue
q = queue.LifoQueue()
for i in range(5):
q.put(i)
while not q.empty():
print(q.get(), end=" ")
26.3.3 优先级队列queue.PriorityQueue
PrirotiyQueue也是继承自Queue类,从中取数据时,是按优先级来取的,放入数据时,要给数据指定一个优先级。优先级是一个数字,数字越小优先级越高,越优先被取出来。
class PriorityQueue(Queue):
'''Variant of Queue that retrieves open entries in priority order (lowest first).
Entries are typically tuples of the form: (priority number, data).
'''
def _init(self, maxsize):
self.queue = []
def _qsize(self):
return len(self.queue)
def _put(self, item):
heappush(self.queue, item)
def _get(self):
return heappop(self.queue)
可以看到获取数据的方法get和数据的方法put都是依赖于Python中堆模块heapq实现的。放入数据时put方法,格式是个元组,第一个元素是priority number,第二个元素才是data。
举一个例子看看:
from queue import PriorityQueue
pq = PriorityQueue()
student_1 = {'name': 'lisi', 'age': 30}
student_2 = {'name': 'wangwu', 'age': 29}
pq.put((20, student_1)) # 第一个参数指定一个优先级
pq.put((9, student_2))
# 越小的数字代表越高的优先级
while not pq.empty():
print(pq.get()) # 按照优先级取 ,先取出student_2再取出student_1
26.3.4 双端队列collections.deque
deque是双端队列(double-ended queue)的缩写,deque能在两端操作,所以deque支持丰富的操作方法。
在Pycharm中输入下面的代码可以查看deque类的所有方法。
import collections
print(dir(collections.deque))
# ['__add__', '__bool__', '__class__', '__contains__', '__copy__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'appendleft', 'clear', 'copy', 'count', 'extend', 'extendleft', 'index', 'insert', 'maxlen', 'pop', 'popleft', 'remove', 'reverse', 'rotate']
按住command键,鼠标左键点击deque。可以进入到deque类的源码文件。源码中有每个方法的使用说明。下面就几个常用的方法举个例子:
import collections
d1 = collections.deque(maxlen=5)
# 右边操作
d1.extend('abcdefg')
print('extend right:', d1)
d1.append('h')
print('append right:', d1)
d1.rotate(2)
print('Rotate the deque 2 steps to the right ', d1)
print(d1.pop())
print(d1)
# 左边操作
d2 = collections.deque()
d2.extendleft(range(6))
print('extend left:', d2)
d2.appendleft(6)
print('append left:', d2)
print(d2.popleft())
print(d2)
# 像列表一样操作
print(d2[0], d2[-1])
d2.remove(3)
print(d2)
print(d2.count(0))
deque是线程安全的,也就是说同时从deque的左边和右边进行操作不会相互影响,看下面的代码:
import collections
import threading
import time
candle = collections.deque(maxlen=20)
candle.extend("abcdefghijklmn")
def burn(direction, nextSource):
while True:
try:
next = nextSource()
except IndexError:
break
else:
print('%s : %s' % (direction, next))
time.sleep(0.1)
print("done %s" % direction)
return
left = threading.Thread(target=burn, args=('left', candle.popleft))
right = threading.Thread(target=burn, args=('right', candle.pop))
left.start()
right.start()
left.join()
right.join()
deque中的maxlen参数非常有用,利用deque中的maxlen创建一个固定长度的队列,通过append往里面添加元素,当元素个数达到maxlen时,最先加入deque的元素自动被移除,新元素被加入。从而始终保持最新的maxlen个元素在deque中。
来看一个例子,在字符串组成的列表中,搜索匹配的字符串,当第一次搜索到时,就打印当前以及前面的n个字符串。这个场景中,我们需要保存有限个元素,下面看下代码:
from collections import deque
def search(lst, pattern, history): # 当编写搜索某一项记录的代码时,通常会用到含有yield关键字的生成器。这将处理搜索过程的代码和使用搜索结果的代码解耦
previous_elements = deque(maxlen=history)
for l in lst:
if pattern in l:
yield l, previous_elements
break
previous_elements.append(l)
if __name__ == '__main__':
persons = ['I', 'am', 'learning', 'python', 'by', 'coding']
for item, records in search(persons, 'b', 2):
for r in records:
print(r, end=' ')
print(item)
deque(maxlen=history)
创建了一个长度为history的固定长度队列。当有新的元素加入队列但此时队列已满时会自动移除最老的那条记录。
26.4 面试题
- 匹配文件中每一行,当匹配时,输出匹配行以及刚刚进行过匹配操作的N行
from collections import deque
def search(text, pattern, history):
previous_lines = deque(maxlen=history)
for line in text:
if pattern in line:
yield line, previous_lines
previous_lines.append(line)
if __name__ == '__main__':
with open("java.txt") as f:
for line, previous in search(f, 'javascript', 2):
for p in previous:
print(p, end='')
print(line)
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
说明:
你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
class Solution:
def topKFrequent(self, nums, k: int):
if k<0 or k>len(nums): return []
from queue import PriorityQueue
from collections import defaultdict
queue = PriorityQueue() # 优先级队列
d = defaultdict(int) # 值是int的字典
res = []
for i in nums:
d[i]+=1
d = list(d.items())
print(d)
for i in range(len(d)):
queue.put([-d[i][1],d[i][0]]) # 这里第一个参数要用,要用负数,优先级队列中越小优先级越高
for i in range(k):
res.append(queue.get()[1])
return res