[英]Python Asyncio/Trio for Asynchronous Computing/Fetching
我正在寻找一种方法来有效地从磁盘中获取一大块值,然后在该大块上执行计算/计算。 我的想法是一个 for 循环,它首先运行磁盘获取任务,然后对获取的数据运行计算。 我想让我的程序在运行计算时获取下一批数据,这样我就不必在每次计算完成时都等待另一个数据获取。 我预计计算将花费比从磁盘获取数据更长的时间,并且由于单个计算任务已经将 CPU 使用率固定在接近 100%,因此可能无法真正并行完成。
我在下面的 python 中使用 trio 提供了一些代码(但也可以与 asyncio 一起使用以达到相同的效果)来说明我使用异步编程执行此操作的最佳尝试:
import trio
import numpy as np
from datetime import datetime as dt
import time
testiters=10
dim = 6000
def generateMat(arrlen):
for _ in range(30):
retval= np.random.rand(arrlen, arrlen)
# print("matrix generated")
return retval
def computeOpertion(matrix):
return np.linalg.inv(matrix)
def runSync():
for _ in range(testiters):
mat=generateMat(dim)
result=computeOpertion(mat)
return result
async def matGenerator_Async(count):
for _ in range(count):
yield generateMat(dim)
async def computeOpertion_Async(matrix):
return computeOpertion(matrix)
async def runAsync():
async with trio.open_nursery() as nursery:
async for value in matGenerator_Async(testiters):
nursery.start_soon(computeOpertion_Async,value)
#await computeOpertion_Async(value)
print("Sync:")
start=dt.now()
runSync()
print(dt.now()-start)
print("Async:")
start=dt.now()
trio.run(runAsync)
print(dt.now()-start)
此代码将通过生成 30 个随机矩阵来模拟从磁盘获取数据,这会使用少量 cpu。 然后它将对生成的矩阵执行矩阵求逆,它使用 100% cpu(在 numpy 中使用 openblas/mkl 配置)。 我通过对同步和异步操作进行计时来比较运行任务所花费的时间。
据我所知,这两个作业完成的时间完全相同,这意味着异步操作并没有加快执行速度。 观察每个计算的行为,顺序操作按顺序运行提取和计算,异步操作首先运行所有提取,然后再进行所有计算。
有没有办法使用异步获取和计算? 也许与期货或诸如 gather() 之类的东西? Asyncio 具有这些功能,而 trio 在单独的 package trio_future中具有它们。 我也对通过其他方法(线程和多处理)的解决方案持开放态度。
我相信可能存在一种多处理解决方案,可以使磁盘读取操作在单独的进程中运行。 但是,进程间通信和阻塞会变得很麻烦,因为由于 memory 约束,我需要某种信号量来控制一次可以生成多少块,并且多处理往往非常繁重和缓慢。
编辑
谢谢 VPfB 的回答。 我无法在操作中睡眠(0) ,但我认为即使我这样做了,它也必然会阻止计算以支持执行磁盘操作。 我认为这可能是 python 线程和异步的硬限制,它一次只能执行 1 个线程。 如果两个不同的进程都需要等待某些外部资源从您的 CPU 响应,那么同时运行两个不同的进程是不可能的。
也许有一种方法可以使用多处理池的执行程序。 我在下面添加了以下代码:
import asyncio
import concurrent.futures
async def asynciorunAsync():
loop = asyncio.get_running_loop()
with concurrent.futures.ProcessPoolExecutor() as pool:
async for value in matGenerator_Async(testiters):
result = await loop.run_in_executor(pool, computeOpertion,value)
print("Async with PoolExecutor:")
start=dt.now()
asyncio.run(asynciorunAsync())
print(dt.now()-start)
虽然计时,它仍然需要与同步示例相同的时间量。 我想我将不得不使用更复杂的解决方案 go,因为 async 和 await 似乎是一种过于粗糙的工具,无法正确执行此类任务切换。
我不使用 trio,我的回答是基于 asyncio。
在这种情况下,我看到的提高 asyncio 性能的唯一方法是将计算分解成更小的部分并在它们之间插入await sleep(0)
。 这将允许数据获取任务运行。
Asyncio 使用协作调度。 同步 CPU 绑定例程不合作,它会在运行时阻塞其他一切。
sleep()
总是挂起当前任务,允许其他任务运行。将延迟设置为 0 可提供优化路径以允许其他任务运行。 长时间运行的函数可以使用它来避免在函数调用的整个持续时间内阻塞事件循环。
(引自: asyncio.sleep )
如果这是不可能的,请尝试在executor 中运行计算。 这为纯异步代码添加了一些多线程功能。
异步 I/O 的重点是让编写程序变得容易,因为那里有很多网络 I/O 但很少有实际计算(或磁盘 I/O)。 这适用于任何异步库(Trio 或 asyncio)甚至不同的语言(例如 C++ 中的 ASIO)。 所以你的程序在理想情况下不适合异步 I/O! 您将需要使用多个线程(或进程)。 尽管公平地说,包括 Trio 在内的异步 I/O 可用于协调线程上的工作,并且在您的情况下可能会很好地工作。
正如 VPfB 的回答所说,如果您使用 asyncio,那么您可以使用执行程序,特别是传递给loop.run_in_executor()
的ThreadPoolExecutor
。 对于 Trio,等效项是trio.to_thread.run_sync()
(另请参阅 Trio 文档中的线程(如果必须) ),它更易于使用。 在这两种情况下,您都可以await
结果,因此该函数在单独的线程中运行,而主 Trio 线程可以继续运行您的异步代码。 你的代码最终看起来像这样:
async def matGenerator_Async(count):
for _ in range(count):
yield await trio.to_thread.run_sync(generateMat, dim)
async def my_trio_main()
async with trio.open_nursery() as nursery:
async for matrix in matGenerator_Async(testiters):
nursery.start_soon(trio.to_thread.run_sync, computeOperation, matrix)
trio.run(my_trio_main)
计算函数( generateMat
和computeOperation
)不需要是异步的。 事实上,如果它们是有问题的,因为您不能再在单独的线程中运行它们。 一般来说,只有在需要await
某些东西或使用async with
或async for
时才使函数async
。
你可以从上面的例子中看到如何将数据传递给另一个线程中运行的函数:只需将它们作为参数传递给trio.to_thread.run_sync()
,它们就会作为参数传递给函数。 从generateMat()
返回结果也很简单 - 在另一个线程中调用的函数的返回值从await trio.to_thread.run_sync()
返回。 获取computeOperation()
的结果比较棘手,因为它是在computeOperation()
调用的,所以它的返回值被丢弃了。 您需要向它传递一个可变参数(如dict
)并将结果存储在那里。 但要注意线程安全; 最简单的方法是将一个新对象传递给每个协程,并且仅在 Nurseries 完成后检查它们。
您可能可以忽略的一些最后脚注:
yield await
并不是某种特殊的语法。 它只是await foo()
,它在foo()
完成后返回一个值,然后是该值的yield
。to_thread.run_sync()
的线程数,方法是传递一个CapacityLimiter
对象,或者找到默认值并设置计数。 看起来默认值当前为 40,因此您可能希望将其调低一点,但这可能不太重要。为了补充我的其他答案(它像你问的那样使用 Trio),这里是如何使用它只使用没有任何异步库的线程。 使用Future
对象和ThreadPoolExecutor
执行此操作的最简单方法。
futures = []
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
for matrix in matGenerator(testiters):
futures.append(executor.submit(computeOperation, matrix))
results = [f.result() for f in futures]
该代码实际上与异步代码非常相似,但如果有的话,它更简单。 如果您不需要进行网络 I/O,则最好使用此方法。
我认为使用多处理并没有看到任何改进的主要问题是 CPU 的 100% 利用率。 它本质上为您留下了类似异步的行为,其中资源偶尔会被释放并用于 I/O 进程。 您可以为 ProcessPoolExecutor 的工作人员数量设置一个限制,这可能会允许 I/O 有足够的空间准备就绪。
免责声明:我对多处理和线程还是陌生的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.