同步任务与异步任务之间的消息传递,以及在不同事件循环中异步任务的消息传递

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 方法后,主线程会等待所有子线程完成执行再继续,从而确保所有事件循环在主线程结束之前正确关闭,避免了上述错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值