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不一样之外,其他用法完全一致。

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Python中,可以使用队列(Queue)来实现多线程之间的安全通信。队列提供了一种线安全的方式来传递数据Python标准库中的queue模块提供了多种队列实现,其中最常用的是`Queue`类和`Queue.Queue`类。以下是一个示例代码,演示了如何使用`Queue`实现多线程之间的通信: ```python import queue import threading # 创建一个队列 q = queue.Queue() # 生产者函数,向队列中放入数据 def producer(): for i in range(5): item = f"Item {i}" q.put(item) print(f"Produced: {item}") # 消费者函数,从队列中取出数据 def consumer(): while True: item = q.get() if item is None: break print(f"Consumed: {item}") q.task_done() # 创建生产者和消费者线程 producer_thread = threading.Thread(target=producer) consumer_thread = threading.Thread(target=consumer) # 启动线程 producer_thread.start() consumer_thread.start() # 等待生产者线程结束 producer_thread.join() # 等待队列中的所有任务被消费完 q.join() # 向队列中放入一个None作为结束标志 q.put(None) # 等待消费者线程结束 consumer_thread.join() ``` 在上面的示例中,我们创建了一个队列`q`,然后定义了一个生产者函数`producer`和一个消费者函数`consumer`。生产者函数通过调用`q.put()`往队列中放入数据,消费者函数通过调用`q.get()`从队列中取出数据。通过使用队列,我们可以确保生产者和消费者之间的数据传递是线安全的。 请注意,使用队列时需要注意以下几点: - `put()`方法向队列中放入数据,`get()`方法从队列中取出数据。 - 如果队列为空,`get()`方法会阻塞直到队列中有数据可取。 - 如果队列已满,`put()`方法会阻塞直到队列有空间可用。 - `task_done()`方法用于通知队列已完成一个任务,可以在生产者和消费者之间同步任务的完成情况。 - `join()`方法用于阻塞主线程,直到队列中的所有任务都被消费完。 希望这个示例能帮助你理解如何使用队列实现多线程之间的通信。如果有任何进一步的问题,请随时提问!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值