簡體   English   中英

如何使用 python 的 asyncio 模塊正確創建和運行並發任務?

[英]How to properly create and run concurrent tasks using python's asyncio module?

我正在嘗試使用 Python 3 的相對較新的asyncio模塊正確理解和實現兩個同時運行的Task對象。

簡而言之,asyncio 似乎旨在通過事件循環處理異步進程和並發Task執行。 它提倡使用await (應用於異步函數)作為等待和使用結果的無回調方式,而不會阻塞事件循環。 (期貨和回調仍然是一個可行的選擇。)

它還提供了asyncio.Task() class,這是Future的專門子類,旨在包裝協程。 最好使用asyncio.ensure_future()方法調用。 asyncio 任務的預期用途是允許獨立運行的任務與同一事件循環中的其他任務“同時”運行。 我的理解是Tasks連接到事件循環,然后自動在await語句之間繼續驅動協程。

我喜歡能夠使用並發任務而不需要使用其中一個Executor類的想法,但我還沒有找到關於實現的詳細說明。

這就是我目前的做法:

import asyncio

print('running async test')

async def say_boo():
    i = 0
    while True:
        await asyncio.sleep(0)
        print('...boo {0}'.format(i))
        i += 1

async def say_baa():
    i = 0
    while True:
        await asyncio.sleep(0)
        print('...baa {0}'.format(i))
        i += 1

# wrap in Task object
# -> automatically attaches to event loop and executes
boo = asyncio.ensure_future(say_boo())
baa = asyncio.ensure_future(say_baa())

loop = asyncio.get_event_loop()
loop.run_forever()

在嘗試同時運行兩個循環任務的情況下,我注意到除非任務具有內部await表達式,否則它將卡在while循環中,從而有效地阻止其他任務運行(很像普通的while循環)。 但是,一旦任務必須(a)等待,它們似乎可以同時運行而沒有問題。

因此, await語句似乎為事件循環提供了一個在任務之間來回切換的立足點,從而產生並發的效果。

帶有內部await的示例 output :

running async test
...boo 0
...baa 0
...boo 1
...baa 1
...boo 2
...baa 2

示例 output沒有內部await

...boo 0
...boo 1
...boo 2
...boo 3
...boo 4

問題

此實現是否通過了asyncio中並發循環任務的“正確”示例?

唯一可行的方法是讓Task提供阻塞點( await表達式)以便事件循環處理多個任務,這是否正確?

是的,任何在您的事件循環中運行的協程都會阻止其他協程和任務運行,除非它

  1. 使用yield fromawait調用另一個協程(如果使用 Python 3.5+)。
  2. 返回。

這是因為asyncio是單線程的; 事件循環運行的唯一方法是沒有其他協程主動執行。 使用yield from / await暫時掛起協程,讓事件循環有機會工作。

您的示例代碼很好,但在許多情況下,您可能不希望長時間運行的代碼不執行在事件循環內運行的異步 I/O。 在這些情況下,使用asyncio.loop.run_in_executor在后台線程或進程中運行代碼通常更有意義。 如果您的任務受 CPU 限制, ProcessPoolExecutor將是更好的選擇,如果您需要執行一些非asyncio友好的 I/O,則將使用ThreadPoolExecutor

例如,您的兩個循環完全受 CPU 限制並且不共享任何狀態,因此最好的性能來自使用ProcessPoolExecutor跨 CPU 並行運行每個循環:

import asyncio
from concurrent.futures import ProcessPoolExecutor

print('running async test')

def say_boo():
    i = 0
    while True:
        print('...boo {0}'.format(i))
        i += 1


def say_baa():
    i = 0
    while True:
        print('...baa {0}'.format(i))
        i += 1

if __name__ == "__main__":
    executor = ProcessPoolExecutor(2)
    loop = asyncio.get_event_loop()
    boo = loop.run_in_executor(executor, say_boo)
    baa = loop.run_in_executor(executor, say_baa)

    loop.run_forever()

您不一定需要yield from xyield from x來控制事件循環。

在您的示例中,我認為正確的方法是執行yield None或等效的簡單yield ,而不是yield from asyncio.sleep(0.001)yield from asyncio.sleep(0.001)

import asyncio

@asyncio.coroutine
def say_boo():
  i = 0
  while True:
    yield None
    print("...boo {0}".format(i))
    i += 1

@asyncio.coroutine
def say_baa():
  i = 0
  while True:
    yield
    print("...baa {0}".format(i))
    i += 1

boo_task = asyncio.async(say_boo())
baa_task = asyncio.async(say_baa())

loop = asyncio.get_event_loop()
loop.run_forever()

協程只是普通的舊 Python 生成器。 在內部, asyncio事件循環會保存這些生成器的記錄,並在永無止境的循環中對每個生成器調用gen.send() 無論何時yield ,對gen.send()的調用gen.send()完成並且循環可以繼續。 (我正在簡化它;查看https://hg.python.org/cpython/file/3.4/Lib/asyncio/tasks.py#l265以獲得實際代碼)

也就是說,如果您需要在不共享數據的情況下進行 CPU 密集型計算,我仍然會選擇run_in_executor路線。

在 Python 3.10 中不推薦使用asyncio.ensure_futureasyncio.get_event_loop函數。

您可以通過asyncio.create_task同時運行兩個協程say_boosay_baa

async def main():
    boo = asyncio.create_task(say_boo())
    baa = asyncio.create_task(say_baa())
    await boo
    await baa

asyncio.run(main())

你也可以使用asyncio.gather

async def main():
    await asyncio.gather(say_boo(), say_baa())

asyncio.run(main())

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM