Python 异步程序和同步程序的交互
问题的提出
在I/O等待处理较多的 Pyhthon 程序中,使用 asycio 异步程序可以大大提高程序的运行效率,并简化程序的开发。我们在开发中使用异步编程时,需要接收来自原有同步程序队列的数据。为此,我们在网络查找了与异步编程有关的资料,发现 Python 的事件循环中有将一个同步函数转换为线程或进程的方法,返回一个 feature 对象后就可以异步执行;与此同时,Python 3.9 以后,asyncio 库中也有了 to_thread 方法实现同样的功能,而且看起来更为简洁。本文的内容就是我们试用这些方法的情况。
官方文档的描述
事件循环的上述功能在官网中有描述其链接是:https://docs.python.org/zh-cn/3/library/asyncio-eventloop.html#executing-code-in-thread-or-process-pools。可以参见其中的“在线程或进程池中执行代码”一节。具体的方法为:
awaitable loop.run_in_executor(executor, func, *args)
其中的 tunc 即是同步的函数或方法。
Python 3.9 以后,asyncio 本身也有了类似的方法:
coroutine asyncio.to_thread(func, /, *args, **kwargs)
具体详见下述官方网页:https://docs.python.org/zh-cn/3/library/asyncio-task.html 的“在线程中运行"一节。
代码实例
下面的例子演示了同步方法和异步方法的混合执行。主程序执行了两个线程,一个是启动异步的主程序,另一个是同步的测试驱动程序。
异步程序 def async main() 中,并发执行两个异步任务,其中一个是执行异步程序 factorial,循环定时打印阶乘的每个因子,最后输出阶乘的值,这个方法摘自官方的示例程序;另一个是转换后执行的同步程序 get_queue_test(), 从 test_queue 中循环阻塞读取 4 次,每次读到消息就打印出来,最后返回一个 ‘got’ 字符串。
同步测试驱动程序每隔1秒钟向 test_queue 中放入一个数字。
下附全部代码,Python 3.9 以后的解释器可以正确执行。如程序中那样,注释掉下面一句,Python 3.7 以后的解释器都可以运行。
L = await asyncio.gather(factorial(“C”, 4), asyncio.to_thread(get_queue_test))
import time
import asyncio
import queue
import threading
test_queue = queue.Queue()
async def factorial(name, number):
f = 1
for i in range(2, number + 1):
print(f"Task {name}: Compute factorial({number}), currently i={i}...")
await asyncio.sleep(1)
f *= i
print(f"Task {name}: factorial({number}) = {f}")
return f
def get_queue_test():
f = 0
while f < 4:
# 读取队列,阻塞方式
f = test_queue.get()
print(f"task sync: {f} received ")
return 'got'
async def sync_trans():
loop = asyncio._get_running_loop()
fut = loop.run_in_executor(None, get_queue_test)
got = await fut
return got
# 生成两个并发运行的任务并执行
async def main():
# 这一句调用了sync_trans,是事件循环的 executor 的例子, 在 sync_trans 中转换和执行。
L = await asyncio.gather(factorial("C", 4), sync_trans())
# 这一句是使用 to_thread 方法的例子,用 to_thread 直接转换和执行 get_queue_test
# L = await asyncio.gather(factorial("C", 4), asyncio.to_thread(get_queue_test))
print(L)
def run_main():
asyncio.run(main())
# 用于测试的驱动,同步程序,向消息队列中放入消息,用的是queue.Queue,而不是 asyncio 的队列。
def put_to_queue():
for i in range(0, 5):
time.sleep(2)
test_queue.put(i)
if __name__ == '__main__':
print('\n\rbegining of the testing!')
# 异步处理线程
t1 = threading.Thread(target=run_main)
# 测试驱动线程
t2 = threading.Thread(target=put_to_queue)
t1.start()
t2.start()
t1.join()
t2.join()
print('end of the testing\n\r')
代码执行结果如下:
begining of the testing!
Task C: Compute factorial(4), currently i=2...
task to thread: 0 received
Task C: Compute factorial(4), currently i=3...
task to thread: 1 received
Task C: Compute factorial(4), currently i=4...
Task C: factorial(4) = 24
task to thread: 2 received
task to thread: 3 received
task to thread: 4 received
[24, 'got']
end of the testing
从执行结果可以看出,get_test_queue 的阻塞 get 操作没有影响 factorial 的执行。