python之多线程编程数据安全的queue模块详细梳理

queue模块是python官方自带模块,它实现了多生产者、多消费者队列,特别适用于在多线程间必须安全地交换消息的场合。

queue模块实现了三种类型的队列,它们都是类,区别仅仅是消息的取回顺序。使用Queue类创建的是先进先出的队列(firt in first out,FIFO),使用LifoQueue类创建的是后进先出的队列(last in first out,LIFO),使用PriorityQueue类创建的是优先级队列。这三种队列内部使用了锁来阻塞竞争线程,即多个线程只能排队轮流使用队列,不能同时并行使用队列。

下面我将详细讲述这三种队列的使用方法,讲述的顺序是先看模块的接口源码、再讲解接口说明、最后列举案例。

先进先出Queue

接口源码

class Queue(Generic[_T]):
    maxsize: int

    mutex: Lock  # undocumented
    not_empty: Condition  # undocumented
    not_full: Condition  # undocumented
    all_tasks_done: Condition  # undocumented
    unfinished_tasks: int  # undocumented
    queue: Any  # undocumented
    def __init__(self, maxsize: int = ...) -> None: ...
    def _init(self, maxsize: int) -> None: ...
    def empty(self) -> bool: ...
    def full(self) -> bool: ...
    def get(self, block: bool = ..., timeout: Optional[float] = ...) -> _T: ...
    def get_nowait(self) -> _T: ...
    def _get(self) -> _T: ...
    def put(self, item: _T, block: bool = ..., timeout: Optional[float] = ...) -> None: ...
    def put_nowait(self, item: _T) -> None: ...
    def _put(self, item: _T) -> None: ...
    def join(self) -> None: ...
    def qsize(self) -> int: ...
    def _qsize(self) -> int: ...
    def task_done(self) -> None: ...

接口说明

  • init方法:实例化方法,默认值参数maxsize(默认值是0表示为不限长度)用来指定队列长度。
  • get方法,从队列中取数据:
    1. 参数有2个,参数一block(可选,默认为True)用来指定是否阻塞,参数二timeout(可选,默认为None)用来指定超时阀值 ,该方法默认状态会阻塞当前进程直至取出数据。
    2. 置block为True且timeout为正浮点数时表示该方法会阻塞当前进程timeout秒,超时后会抛queue.Empty异常。
    3. 设置block为false时即表示该方法不阻塞,进程会直接取数据,若队列空会抛queue.Empty异常。另外当block为false时timeout参数失效。
  • get_nowait方法,功能等价于get(False)即不阻塞取数据。
  • put方法,将数据放入队列:
    1. 参数有3个:参数一item(必选,无默认值)是要存放的数据,参数二block(可选,默认为True)用来指定是否阻塞,参数三timeout(可选,默认为None)用来指定超时阀值 。若给定长队列存放数据遇到队列满时该方法会阻塞当前进程直至存入数据,若给不定长队列存放数据虽不会遇到阻塞问题但存在撑爆内存的可能。
    2. 设置block为True且timeout为正浮点数时表示该方法会阻塞当前进程timeout秒,超时后会抛queue.Full异常。
    3. 设置block为false时即表示该方法不阻塞,进程会直接存数据,如果队列满会抛queue.Full异常。另外当block为false时timeout参数失效。
  • put_nowait方法:参数item为要存放的数据,功能等价于put(item, False)即不阻塞存数据。
  • empty方法:判断队列是否空,若为空返回True,若不空返回False。请注意多线程竞争时存在判断为空的瞬间另一线程put后实际不空的可能。
  • full方法:用来判断队列是否满,若已满返回True,若不满返回False。另外不定长队列永远返回False。请注意多线程竞争时存在刚判断为满的瞬间另一线程get后实际不满的可能。
  • qsize方法:返回队列的大致大小。请注意由于多线程竞争会导致qsize()>0时get()仍可能被阻塞、qsize() < maxsize时put()仍可能被阻塞。
  • taskdone方法:该方法是消费者使用的,通知生产者队列已被取走一个消息。该方法必须配合join方法使用。
  • join方法:该方法是生产者使用的,阻塞进程直到队列空。该方法必须配合takedone方法使用。

案例,Queue中的join和task_done方法:

queue类的put和put_nowait是存消息,get和get_nowait方法是取消息,qsize、full、empty是判断消息队列,这些用法非常简单。我这里重点讲一下极其有用且少有人会用的join和taskdone方法。

以下是生产者、消费者模型的案例:

import time
import random
from threading import Thread
import queue
 
def consumer(q,name):
    while True:
        food = q.get()
        time.sleep(random.uniform(0.1,1))  # 消费快
        print(f'“{name}”吃了“{food}”')
        q.task_done()
 
def producer(q,name):
    for i in range(1,6):
        time.sleep(random.uniform(1,2))  # 生产慢
        food = f'{name}{i}'
        print(f'“{name}店”生产了“{food}”')
        q.put(food)
    q.join()
 
 
if __name__ == '__main__':
    q = queue.Queue(10)
    producer_table = ['蛋糕','面包','冰淇淋']
    for i in producer_table:
        p = Thread(target=producer,args=(q,i))
        p.start()
    customer_table = ['张三','李四','王五','赵六']
    for i in customer_table:
        c = Thread(target=consumer,args=(q,i))
        c.daemon = True
        c.start()

输出:

有兴趣的朋友可以复制代码自行执行,因为每次运行结果都不一致。但是在生产者生产完毕、消费者消费完毕后,程序可以正常结束。

说明:

  • 在生产者线程中所有产品做完后,必须写一行“q.join()”,作用是阻塞本线程直到消费者取走所有产品。
  • 在消费者线程中While True从队列获取消息,每次取消息语句后面必须跟一句“task_done()”,用来通知生产者的队列.join()计数。
  • 将消费者线程设为守护线程。
  • 多个生产者线程和多个消费者线程同时执行,因为生产者是阻塞线程(自己的产品被取完才释放阻塞)而消费者是守护线程(主线程结束后守护线程会跟着主线程结束)。
  • 最终的效果就是生产者不停的生产,生产完成后就等待产品被取走;消费者死循环不停的消费;所有的生产者线程产品被取走后自动关闭;所有的生产者线程关闭后主线程强制关闭所有消费者线程,程序正常结束。
  • 另外上述代码案例中生产者速度慢消费者速度快,程序不会陷入死循环可以正常运行直到任务完成正常退出;假如将生产者和消费者的time.sleep参数互换一下,程序仍然可以正常运行直到任务完成正常退出。该方法比向消息队列中放Null的方式好用多了。强烈建议使用。

后进先出LifoQueue

后进先出队列用法:

from queue import LifoQueue  # 后进先出队列


q = LifoQueue()
for i in range(6):
    q.put(i)
while not q.empty():
    print(q.get())

输出:

5
4
3
2
1
0

说明:

后进先出队列就是栈,除了消息的取回顺序和Queue相反之外,其他用法完全一致。

优先级PriorityQueue

优先级队列使用案例:

from queue import PriorityQueue  # 优先级队列


priq = PriorityQueue()
priq.put((0,'aaa'))
priq.put((1,'bbb'))
priq.put((2,'cccc'))
priq.put((0,'dddd'))
priq.put((0,'eeee'))
priq.put((1,'ffff'))
while not priq.empty():
    print(priq.get()[1])

输出:

aaa
dddd
eeee
bbb
ffff
cccc

说明:

优先级队列的item参数是元组,元组的第一项是优先级号(数字类型,数字越小越早取、数字越大越晚取,可以是整数、浮点数、正数或负数)、第二项是数据。

优先级队列除了消息的取回顺序和Queue不一样之外,其他用法完全一致。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值