1. 需求描述
总所周知,queue.Queue()是线程安全的,一般用于同步任务不同线程之间的通信,其get()方法是阻塞的,当没有数据时,会处于一直等待状态。
asyncio.Queue()一般用于同一事件循环不同异步任务之间的通信,其get方法不是阻塞的,但在同步任务之间、同步与异步任务之间,不同事件循环的异步任务之间,是不能够使用 asyncio.Queue() 消息队列来传递消息的。
通信方式 | 线程 | 事件循环 | 消息队列 | 数据获取方式 |
同步任务之间的通讯 | 不同线程 | 不同事件循环 | queue.Queue() | get() / get_nowait() |
同一事件循环中的异步任务之间的通信 | 同一线程 | 同一事件循环 | asyncio.Queue() / queue.Queue() | get() / get_nowait() |
同步与异步任务之间的通信 | 不同线程 | 不同事件循环 | queue.Queue() | get_nowait() |
不同事件循环的异步任务之间的通信 | 不同线程 | 不同事件循环 | queue.Queue() | get_nowait() |
2. 解决思路
为了解决同步与异步任务之间,不同事件循环的异步任务之间的消息通信,只能使用queue.Queue()完成通信,因此需要实现在异步函数中执行阻塞的同步方法。当事件循环需要从一个线程安全的队列中获取数据时,但由于队列的阻塞操作不能直接在异步环境中执行,因此使用 run_in_executor 方法。主要运用到了queue.Queue()中的get_nowait()方法,以及await asyncio.get_event_loop().run_in_executor(...) 方法。
此方法是一个异步操作,它允许在 asyncio 的事件循环中执行一个阻塞的同步函数或方法,此方法的内部实现机制及原理如下:
在 asyncio 的事件循环中调用一个阻塞操作的同步方法 queue.get(),并通过 run_in_executor 方法将其放入默认的线程池中执行,将同步阻塞方法放入一个线程池中执行。await 关键字等待 run_in_executor 返回结果,一旦执行完成,它会返回queue.get()的结果,这样就能在异步环境中获取线程安全队列 queue 中的数据,而不会阻塞整个事件循环。
3. 代码实战
import asyncio
import threading
import queue
from time import sleep
# 创建线程安全的队列用于跨线程通信
queue_0 = queue.Queue()
def get_from_queue(queuex):
try:
return queuex.get_nowait()
except queue.Empty:
return None
# 分别创建三个线程
def main_task():
thread1 = threading.Thread(target=thread_task_0)
thread2 = threading.Thread(target=thread_task_1)
thread3 = threading.Thread(target=thread_task_2)
thread1.start()
thread2.start()
thread3.start()
thread1.join()
thread2.join()
thread3.join()
# 线程0: 创建一个同步任务,负责生产数据
def thread_task_0():
item = ("message", "00:message from thread_task_0 ")
while True:
queue_0.put(item)
print("produce message: thread_task_0")
sleep(5)
# 线程1 创建一个异步任务,负责生产数据
def thread_task_1():
asyncio.run(async_task_0())
async def async_task_0():
item = ("message", "10:message from async_task_0 ")
while True:
queue_0.put(item)
print("produce message: async_task_0")
await asyncio.sleep(5)
# 线程2 创建两个异步任务,一个负责生产数据,一个负责接收以上所有任务的数据
def thread_task_2():
asyncio.run(run_async_tasks())
async def run_async_tasks():
task1 = asyncio.create_task(async_task_1())
task2 = asyncio.create_task(async_task_2())
await asyncio.gather(task1, task2)
async def async_task_1():
item = ("message", "20:message from async_task_1 ")
while True:
queue_0.put(item)
print("produce message: async_task_1")
await asyncio.sleep(5)
async def async_task_2():
while True:
info = await asyncio.get_event_loop().run_in_executor(
None, get_from_queue, queue_0
)
if info:
print(f"consume message: {info}")
main_task()
线程0 (thread_task_0):
创建一个同步任务,负责生产数据,并放入 queue_0 中。
线程1 (thread_task_1):
创建一个新的事件循环,并运行 async_task_0,该任务负责生产数据并放入 queue_0 中。
线程2 (thread_task_2):
创建一个新的事件循环,并运行两个异步任务:async_task_1 负责生产数据并放入 queue_0 中,async_task_2 负责从 queue_0 中获取数据并消费。
以上代码,模拟了:
同一事件循环异步任务之间的通讯:async_task_1 与 async_task_2
不同事件循环异步任务之间的通信:async_task_0 与 async_task_2
同步任务与异步任务之间的通信:thread_task_0 与 async_task_2
4. 注意事项
使用协程编程,特别是调用 run_in_executor 方法时,容易引发 运行时报错:
RuntimeError: can't register atexit after shutdown
这是因为如果主线程结束后,整个程序进程即将退出,操作系统可能会强制清理所有线程和资源,包括未完成的子线程和它们的事件循环。这种情况下,子线程中的事件循环可能会被强制关闭。
所以需要在创建线程时,保证子线程中的事件循环未结束时,主线不能结束,因此在代码中加入thread1.join():主线程会等待 thread1 完成执行后再继续执行。
如果不使用 join 方法,主线程可能会在两个子线程完成执行之前结束。这可能会导致子线程中运行的事件循环在主线程结束时被关闭,从而引发 RuntimeError: can't register atexit after shutdown 错误。
使用 join 方法后,主线程会等待所有子线程完成执行再继续,从而确保所有事件循环在主线程结束之前正确关闭,避免了上述错误。