我正在尝试深入研究 Python 的 asyncio 模块。我知道我们需要等待协程来获取结果,当我们等待任何协程时,周围的协程会将其执行交给事件循环,并让任何其他已准备好执行的协程运行。
考虑以下示例:
async def parse_data(idx,pool):
await compute_and_fetch_data()
# Do some work
async def compute_and_fetch_data(idx, pool):
# Do some CPU intensive task such as a complex compuation and produce the query to be executed
#Hardcoding query for demonstration
query = "SELECT * FROM customers"
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(query)
result = await cur.fetchall()
logger.info(f"Fetched result for query {idx+1} successfully.")
return result
async def main():
pool = await aiomysql.create_pool(host='localhost', user='user', password='password', db='laundry')
asyncio.gather(parse_data(1, pool))
asyncio.run(main())
在 parse_data 协程内,我们正在等待 compute_and_fetch_data 协程。我添加了一个占位符来表示 compute_and_fetch_data 在对数据库执行查询之前将执行的一些计算。由于计算是一项 CPU 密集型任务,而执行查询是一项 IO 密集型任务,因此 parse_data 协程会在到达
await compute_and_fetch_data
代码块时立即放弃执行,还是首先在 compute_and_fetch_data 内进行计算,然后当到达需要对数据库执行查询的阶段时放弃执行(一个耗时的过程)?
如果后者为真,那么周围的协程在到达
await
关键字时放弃执行的说法仍然有效吗?
是的 -
await
关键字以及 async with
和 async for
语句是要点,然后,允许在同一事件循环中运行其他并发任务。
为了完整起见,请注意,首先计算
await
右侧的表达式 - 在这种情况下,您有一个协同例程函数调用:该调用以同步方式返回一个协同例程对象,并且非常高效 - 此步骤在协同例程函数内部不执行任何代码。只有在解析 await
本身时,协同例程(在本例中为 compute_and_fetch_data
)内的代码才会真正执行 - 并且其他等待任务也可以随之采取步骤。一旦协同例程中的代码启动,它将在异步循环线程上单独运行,在解决其中的第一个 async with
代码之前不会被中断。因此,请注意,用占位符替换的计算将有效地阻止任何其他要运行的异步代码。通常的方法是将计算代码重构为另一个同步函数,并使用 await asyncio.run_in_executor(...)
来实现。 (请注意,具有 GIL 的 Python 仍然会使您的代码以次优方式运行:asyncio 循环将可以自由地执行其他任务,但如果计算代码是纯 Python 代码,则两个线程之一将实际运行。这次,解决方法是检查您的执行程序代码,看看是否值得使用 ProcessPoolExecutor
而不是默认的 ThreadPoolExecutor
asyncio 使用 - 在启动工作程序和序列化所需的资源方面存在权衡一方面是要计算的数据,另一方面是通过利用其他 CPU 内核运行真正并行代码的能力,这是多处理所允许的。