Python 中高级知识 queue

Queue

Queue是Python标准库中的线程安全的队列实现,提供了一个适用于多线程编程的有序队列结构,用来在生产者和消费者线程之间的信息传递.

模块实现了三种类型的队列,它们的区别仅仅是条目取回的顺序。在FIFO队列中,先添加的任务先取回。在LIFO队列中,最近被添加的条目先取回(操作类似一个堆栈)。优先级队列中,条目将保持排序(使用heapq模块) 并且最小值的条目第一个返回。

在内部,这三个类型的队列使用锁来临时阻塞竞争线程,保证多线程的安全;然而,它们并未被设计用于线程的重入性处理,所以在涉及锁重入的时候要格外注意

源码节选:

__all__ = ['Empty''Full''Queue''PriorityQueue''LifoQueue''SimpleQueue']

Queue

FIFO 队列

Queue 提供了一个基本的 FIFO 容器,使用方法很简单,maxsize 是个整数,指明了队列中能存放的数据个数的上限。一旦达到上限,插入会导致阻塞,直到队列中的数据被消费掉。如果maxsize 小于或者等于 0,队列大小没有限制。

典型错误

# !/usr/bin/env python3
# -*- coding: utf-8 -*
 
from queue import Queue
 
q = Queue(5)
 
# put 5 次 后被阻塞,导致程序死锁
for x in range(10):
    q.put(x)
 
for x in range(10):
    print(q.get())

因为这个队列的最大容量为5, 连续放入超过5个的元素,并且没有其他线程去消费,会导致阻塞。比如上面的这段无法继续执行下去。这里是一个错误写法,初学者经常会犯这类问题。

正确修正写法

# !/usr/bin/env python3
# -*- coding: utf-8 -*
 
from queue import Queue
 
q = Queue(15)
 
for x in range(10):
    q.put(x)
 
for x in range(10):
    print(q.get())

修正手段:扩大queue的容量到足够大行。其实queue的默认就是无穷大,直到机器的物理资源不够!

本质

queue 是一个生产-消费模型,像上面的代码,不是并发的出现生产和消费,很容易出现存满或者取空的情况

典型的操作类似这样

# !/usr/bin/env python3
# -*- coding: utf-8 -*
import random
from queue import Queue
from threading import Thread
 
 
# 生产者
def producer(q: Queue):
    while True:
        i = q.put(random.randint(110))
        print("producer put ", i)
 
 
# 消费者
def consumer(q: Queue):
    while True:
        i = q.get()
        print("consumer get ", i)
 
 
if __name__ == "__main__":
    q = Queue(5)
    p1 = Thread(target=producer, args=(q,))
    c1 = Thread(target=consumer, args=(q,))
 
    p1.start()
    c1.start()
    p1.join()
    c1.join()

一些线程生产,一些线程消费

....
 
consumer get  6
producer put  None
consumer get  10
producer put  None
consumer get  8
producer put  None
consumer get  7
producer put  None
consumer get  9
producer put  None
consumer get  9
....

和get/put类似的是get_nowait/put_nowait,不同的是后者实际是不具备阻塞等待能力的,英文也暗示了。如果队列取空了再取/队列填满了再填就会报错

LifoQueue

LIFO 队列,除了元素出入顺序和queue不一样,其余的一样

PriorityQueue

优先级队列,可以根据你给的比较规则排序,使你取到的元素,总是当前比较排序出的最小的元素,排序规则是heapq模块的堆排序

将前面演示的代码略微改下,注意看下注释

# !/usr/bin/env python3
# -*- coding: utf-8 -*
import random
from queue import Queue, PriorityQueue
from threading import Thread
 
 
# 生产者
def producer(q: Queue):
    while True:
        # 放入可排序的对象,这里是int,它可以排序
        i = q.put(random.randint(110))
        if q.qsize() == 5:
            q.join()
        print("producer put ", i)
 
 
# 消费者
def consumer(q: Queue):
    while True:
        i = q.get()
        # 这里注释了,故意让q.join阻塞,方便看到排序后队列被消费,可以解屏蔽join就不会阻塞了
        # q.task_done()
        print("consumer get ", i)
 
 
if __name__ == "__main__":
    q = PriorityQueue(5)
    p1 = Thread(target=producer, args=(q,))
    p1.start()
    c1 = Thread(target=consumer, args=(q,))
    c1.start()
    p1.join()
    c1.join()

元素在队列里面排序,然后被排序并消费

producer put  None
producer put  None
producer put  None
producer put  consumer get  1
None
producer put  None
consumer get  2
consumer get  4
consumer get  4
consumer get  6
consumer get  7

自定义排序规则

# !/usr/bin/env python3
# -*- coding: utf-8 -*
import random
from queue import Queue, PriorityQueue
from threading import Thread
 
 
class MyValue:
    def __init__(self, priority, description):
        self.priority = priority
        self.description = description
        return
         
    # 重写<运算,供比较操作
    def __lt__(self, other):
        # 改变这里的< 为 > ,优先级顺序就不一样了
        return self.priority - other.priority < 0
 
    def to_str(self):
        return "priority : " + str(self.priority) + "description: " + str(self.description)
 
 
# 生产者
def producer(q: Queue):
    while True:
        # 放入可排序的对象,这里是int,它可以排序
        i = random.randint(110)
        q.put(MyValue(i, "value" + str(i)))
        if q.qsize() == 5:
            q.join()
 
 
# 消费者
def consumer(q: Queue):
    while True:
        i = q.get()
        # 这里注释了,故意让q.join阻塞,方便看到排序后队列被消费,可以解屏蔽join就不会阻塞了
        # q.task_done()
        print("consumer get ", i.to_str())
 
 
if __name__ == "__main__":
    q = PriorityQueue(5)
    p1 = Thread(target=producer, args=(q,))
    p1.start()
    c1 = Thread(target=consumer, args=(q,))
    c1.start()
    p1.join()
    c1.join()

SimpleQueue

其实使用和Queue没有区别,但是使用比Queue简单,什么参数都不需要传入,直接创建实例了就可以使用实例,有最基本的Queue的方法和特性

# !/usr/bin/env python3
# -*- coding: utf-8 -*
import random
from queue import Queue, SimpleQueue
from threading import Thread
 
 
# 生产者
def producer(q: Queue):
    while True:
        i = q.put(random.randint(110))
        print("producer put ", i)
 
 
# 消费者
def consumer(q: Queue):
    while True:
        i = q.get()
        print("consumer get ", i)
 
 
if __name__ == "__main__":
    q = SimpleQueue()
    p1 = Thread(target=producer, args=(q,))
    c1 = Thread(target=consumer, args=(q,))
 
    p1.start()
    c1.start()
    p1.join()
    c1.join()

task_done/join

一般我们使用put/get就行了,为什么要有get后显式使用task_done,并期待在全部的get没有都完成task_done前,生产者使用join会阻塞?

本质还是生产-消费模型的考虑,一般的put/get模型,双方都是没有沟通的,彼此透明,不管对方。但是有这个有的情况,生产者的全速生产速度为10个/S, 消费者的消费全速为8个/S,如果继续采用put/get模型会出现生产-消费速度不一样的问题,生产者会生产过快填满队列或者队列越来越长指导硬件资源耗尽,有了task_done和join就做了一个很好的生产消费协调,生产者生产一段时间后可以自己按照条件检查并自己join阻塞自己暂停生产,等待消费者消费完后再次投入生产,是个很典型的类似限流的操作

FULL

对满的 Queue 对象,调用非阻塞的 put() (or put_nowait()) 时,引发的异常。

# !/usr/bin/env python3
# -*- coding: utf-8 -*
from queue import Queue
 
q = Queue(4)
 
for x in range(10):
    i = q.put(block=False, item=x)
    print(i)

非阻塞填满后,异常返回

None
None
None
None
Traceback (most recent call last):
  File "C:\INSTALL\Python3\lib\queue.py", line 136, in put
    raise Full
queue.Full

EMPTY

对空的 Queue 对象,调用非阻塞的 get() (or get_nowait()) 时,引发的异常。

# !/usr/bin/env python3
# -*- coding: utf-8 -*
from queue import Queue
 
q = Queue(4)
 
for x in range(3):
    i = q.put(block=False, item=x)
    print(i)
 
for x in range(5):
    i = q.get(block=False)
    print(i)

非阻塞取空后,异常返回

Traceback (most recent call last):
  File "C:\INSTALL\Python3\lib\queue.py", line 167in get
    raise Empty
_queue.Empty
None
None
None
0
1
2
 
posted @ 2021-09-26 10:40  旁人X  阅读(173)  评论(0)    收藏  举报