asyncio理解(未允禁轉(zhuǎn))

本文介紹asyncio的基本用法,asyncio通過event loop機制瘋狂調(diào)度和執(zhí)行各個coroutine

關(guān)鍵字-async/await

  • async def用于定義一個coroutine. 一個coroutine需要被壓入一個事件循環(huán)才能被執(zhí)行,方式有兩種:the code inside the coroutine function won't run until you await on the function, or run it as a task to a loop

  • await用于等待一個awaitable對象,例如future實例和coroutine實例。更多future vs coroutine的介紹見于下文

    協(xié)程中使用await可以讓出cpu,讓event loop可以去執(zhí)行其他協(xié)程

    當(dāng)一個線程中的 asyncio 事件循環(huán)遇到 await 表達(dá)式時,線程的事件循環(huán)機制會暫停當(dāng)前協(xié)程的執(zhí)行,并嘗試運行其他就緒的協(xié)程。在這個過程中,線程仍然處于運行狀態(tài)。然而,由于事件循環(huán)可以在協(xié)程之間切換,這使得線程能夠在等待 I/O 或其他異步操作期間執(zhí)行其他任務(wù)

基本概念-future vs coroutine

  • 相同點:兩者都是awaitable對象
  • 不同點:
    實現(xiàn)方式上存在根本差異
    • Future 是一個表示未來結(jié)果的對象,它通常由一個 asyncio 任務(wù)(通過asyncio.create_task或者loop.create_task創(chuàng)建)或其他低級別的異步操作創(chuàng)建,且通常用于封裝底層的異步操作如網(wǎng)絡(luò)請求、文件 I/O 等

      下面給出一個關(guān)于future創(chuàng)建和使用的基本示例

      import asyncio
      
      async def main():
          # return the loop instance specific to the current thread
          loop = asyncio.get_event_loop()
          # create an default future
          future = loop.create_future()
      
          async def set_future_result():
              await asyncio.sleep(1)
              future.set_result("Hello, world!")
              print(f"future done: {future.done()}")
          # use another coroutine to set future
          loop.create_task(set_future_result())
      
          try:
              print("coro main waiting")
              result = await future
              print("coro main wait done")
              print(f"result is {result}")
          except Exception as e:
              print(f"An error occurred: {e}")
      
      loop = asyncio.new_event_loop()
      loop.run_until_complete(main())
      loop.close()
      

      程序輸出:

      coro main waiting
      future done: True
      coro main wait done
      result is Hello, world!
      

      asyncio.sleep這個coroutine的實現(xiàn)邏輯正是運用了類似上面的future創(chuàng)建和使用邏輯

      async def sleep(delay, result=None, *, loop=None):
          """Coroutine that completes after a given time (in seconds)."""
          if loop is not None:
              warnings.warn("The loop argument is deprecated since Python 3.8, "
                          "and scheduled for removal in Python 3.10.",
                          DeprecationWarning, stacklevel=2)
      
          if delay <= 0:
              await __sleep0()
              return result
      
          if loop is None:
              loop = events.get_running_loop()
      
          future = loop.create_future()
          h = loop.call_later(delay,
                              futures._set_result_unless_cancelled,
                              future, result)
          try:
              return await future
          finally:
              h.cancel()
      
    • coroutine 是一個使用 async def 定義的特殊函數(shù)

      • 它的內(nèi)部可以使用(但不強制使用) await 關(guān)鍵字等待其他 coroutine 或 Future 對象的返回值
      • 一個coroutine必須 1. 通過loop.create_task提交到loop實例的任務(wù)隊列,2. 或者被其他coroutine await以加入loop實例的任務(wù)隊列。該coroutine才能夠被loop調(diào)度運行

      注意,coroutine內(nèi)部邏輯不可以【同步阻塞】當(dāng)前線程,否則整個事件循環(huán)將被阻塞

      下面給出兩個sleep功能的coroutine實現(xiàn),包括阻塞式實現(xiàn)和非阻塞式實現(xiàn)

      import asyncio
      import time
      
      async def blocking_sleep():
          """
          blocking_sleep 調(diào)用 time.sleep(1) 阻塞當(dāng)前線程
          當(dāng)前線程將被操作系統(tǒng)掛起1s,該線程上運行的事件循環(huán)自然隨之被阻塞
          """
          time.sleep(1)
      
      async def non_blocking_sleep():
          """
          non_blocking_sleep 不會阻塞當(dāng)前線程
      
          因為asyncio.sleep(1)本身是一個coroutine,其底層邏輯是讓 thread-specific loop 執(zhí)行一個延時協(xié)程。整個過程涉及多次await主動讓出cpu,從而避免阻塞該線程上運行的事件循環(huán)
          """
          await asyncio.sleep(1)
      

loop.run_until_complete vs loop.run_forever

它們都是啟動一個event loop的入口方法

  • run_until_complete(future)

    這個方法用于運行事件循環(huán),直到給定的協(xié)程或 asyncio.Future 對象完成。當(dāng)協(xié)程完成時,run_until_complete 將返回協(xié)程的結(jié)果(如果有),并停止事件循環(huán)。這是在事件循環(huán)中執(zhí)行單個協(xié)程的主要方法

  • run_forever()

    這個方法用于運行事件循環(huán),直到顯式停止。它不接受協(xié)程或 Future 對象作為參數(shù)。當(dāng)你調(diào)用 run_forever() 時,事件循環(huán)將開始一直運行,直到你顯式調(diào)用 loop.stop() 方法。這個方法通常用于長時間運行的應(yīng)用程序,例如網(wǎng)絡(luò)服務(wù)器或后臺任務(wù)處理器。

    為了在 run_forever() 模式下執(zhí)行協(xié)程,你需要使用 asyncio.create_task() 或 loop.create_task() 在事件循環(huán)中創(chuàng)建任務(wù)。這些任務(wù)將在事件循環(huán)運行時被調(diào)度執(zhí)行。

其他注意事項

create_task不會啟動事件循環(huán)

loop.create_task() asyncio.create_task都是用于將一個 coroutine(協(xié)程)包裝為一個 Task 對象并將其加入到事件循環(huán)中。Task 對象是 asyncio.Future 的子類

方法本身不會啟動事件循環(huán),它只是將協(xié)程加入到事件循環(huán)的待執(zhí)行隊列中。要啟動事件循環(huán)并運行協(xié)程,你需要使用如 loop.run_until_complete()loop.run_forever() 等方法

loop.run_in_executor vs async.to_thread

兩者都會啟動另一個獨立線程去完成一些阻塞邏輯,區(qū)別在于:

  • 執(zhí)行時間

    • loop.run_in_executor即時起了一個線程,直接開始執(zhí)行,返回一個future,這個future按需await
    • async.to_thread將新線程包裝成coroutine并返回,必須await或者loop run才能被調(diào)度執(zhí)行
  • 是否依賴loop

    • 獨立線程與事件循環(huán)所在線程相互獨立互不影響,因此loop.run_in_executor()會馬上在executor中執(zhí)行線程邏輯,而不依賴loop的啟動
    • async.to_thread將新線程包裝成coroutine,必須在loop啟動后才能被調(diào)度執(zhí)行

loop的線程安全問題

在 Python 的 asyncio 庫中,事件循環(huán)不是線程安全的,這意味著在多線程環(huán)境中使用事件循環(huán)可能會導(dǎo)致未定義的行為或錯誤。以下是一些與事件循環(huán)線程安全相關(guān)的問題和注意事項:

  • 不要多線程共享同一個事件循環(huán)實例。每個線程應(yīng)該有自己的事件循環(huán)實例

  • 不要在一個線程中操作另一個線程的事件循環(huán)實例。例如,不要在一個線程中調(diào)用另一個線程的事件循環(huán)的 create_task()run_until_complete() 等方法。這可能會導(dǎo)致競態(tài)條件或其他線程安全問題

    如果你確實需要在一個線程中與另一個線程的事件循環(huán)交互,可以使用 asyncio.run_coroutine_threadsafe() 函數(shù)。這個函數(shù)可以安全地【將一個協(xié)程加入到另一線程的事件循環(huán)中】,并返回一個線程安全的 concurrent.futures.Future 對象。你可以在當(dāng)前線程中等待這個 Future 對象,以獲取協(xié)程的執(zhí)行結(jié)果

  • 對于 I/O-bound 任務(wù)或其他長時間運行的操作,可以使用 loop.run_in_executor() asyncio.to_thread() 方法將I/O-bound任務(wù)委托給別的線程執(zhí)行

以下代碼演示了如何在線程B中正確操作線程A中的future對象

import asyncio
import time


async def main():
    # return the loop instance specific to the current thread
    loop = asyncio.get_event_loop()
    # create an default future
    future = loop.create_future()

    def set_future_result():
        time.sleep(1)
        future.set_result("Hello, world!")
        print(f"future done: {future.done()}")

    # # use common thread to set future -> not allowed since thread unsafe
    # import threading
    # t = threading.Thread(target=set_future_result)
    # t.start()

    # use loop.run_in_executor instead
    loop.run_in_executor(None, set_future_result)

    try:
        print("coro main waiting")
        result = await future
        print("coro main wait done")
        print(f"result is {result}")
    except Exception as e:
        print(f"An error occurred: {e}")

loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()

asyncio基本用法示例

asyncio就是使用一個loop對象,對協(xié)程進(jìn)行event loop式執(zhí)行

下面給出一個代碼示例,代碼邏輯為

  • 創(chuàng)建一個loop對象
  • submit_coroutines_blocking不斷向loop提交任務(wù)協(xié)程
  • 注意,submit_coroutines_blocking是阻塞的,因此通過asyncio.to_thread委托給另一獨立線程執(zhí)行。asyncio.to_thread返回的是一個coroutine,需要loop啟動后才能被調(diào)度執(zhí)行
  • 通過loop.run_until_complete或者loop.run_forever啟動loop。(注意,run_forever表示讓loop對象一直進(jìn)行事件循環(huán)。調(diào)用loop.run_forever之前不一定需要先往loop進(jìn)行create_task塞入任務(wù),可以先空跑起來,然后通過其他手段往loop里塞任務(wù))
import asyncio
import time

# 任務(wù)協(xié)程
async def my_coroutine(i):
    print(f"eventloop thread: Started coroutine {i}")
    await asyncio.sleep(5)
    print(f"eventloop thread: Finished coroutine {i}")

def submit_coroutines_blocking(loop: asyncio.AbstractEventLoop):
    """
    submit_coroutines_blocking 是一個無限循環(huán)
    
    每次循環(huán)將向loop提交一個任務(wù)協(xié)程,并且通過time.sleep來模擬阻塞狀態(tài)
    """
    i = 0
    try:
        while loop.is_running():
            # `submit_coroutines_blocking`將在一個單獨的線程中運行,因此不能直接通過`loop.create_task(my_coroutine(i))`訪問原線程的loop對象
            # loop.create_task(my_coroutine(i))
            
            # -> use asyncio.run_coroutine_threadsafe instead
            asyncio.run_coroutine_threadsafe(my_coroutine(i), loop)
            print(f"separated thread: submit coroutine {i} to loop, and going to sleep 2")
            time.sleep(2)  # 阻塞 2 秒提交一個新的協(xié)程
            i += 1
    except Exception as e:
        print(f"Caught exception: {e}")
        loop.stop()


async def submit_coroutines_coro(loop: asyncio.AbstractEventLoop):
    # 將阻塞式的submit_coroutines_blocking委托給單獨的線程處理
    await asyncio.to_thread(submit_coroutines_blocking, loop)
    # await loop.run_in_executor(None, submit_coroutines_blocking, loop) # is ok too
        

loop = asyncio.new_event_loop()
loop.run_until_complete(submit_coroutines_coro(loop))
#####
# or
# loop.create_task(submit_coroutines_coro(loop))
# loop.run_forever()
#####

輸出如下:

separated thread: submit coroutine 0 to loop, and going to sleep 2
eventloop thread: Started coroutine 0
separated thread: submit coroutine 1 to loop, and going to sleep 2
eventloop thread: Started coroutine 1
separated thread: submit coroutine 2 to loop, and going to sleep 2
eventloop thread: Started coroutine 2
eventloop thread: Finished coroutine 0
separated thread: submit coroutine 3 to loop, and going to sleep 2
eventloop thread: Started coroutine 3
eventloop thread: Finished coroutine 1
separated thread: submit coroutine 4 to loop, and going to sleep 2
eventloop thread: Started coroutine 4
eventloop thread: Finished coroutine 2
separated thread: submit coroutine 5 to loop, and going to sleep 2
eventloop thread: Started coroutine 5
eventloop thread: Finished coroutine 3
...

當(dāng)然,我們也可以借助loop.run_in_executorsubmit_coroutines_blocking放到一個單獨線程中執(zhí)行


import asyncio
import time

# 任務(wù)協(xié)程
async def my_coroutine(i):
    print(f"eventloop thread: Started coroutine {i}")
    await asyncio.sleep(5)
    print(f"eventloop thread: Finished coroutine {i}")


def submit_coroutines_blocking(loop: asyncio.AbstractEventLoop):
    """
    submit_coroutines_blocking 是一個無限循環(huán)

    每次循環(huán)將向loop提交一個任務(wù)協(xié)程,并且通過time.sleep來模擬阻塞狀態(tài)
    """
    i = 0
    try:
        while True:
            if not loop.is_running():
                print("loop is not running, continue")
                time.sleep(2)  # 阻塞 2 秒提交一個新的協(xié)程
                continue

            # `submit_coroutines_blocking`將在一個單獨的線程中運行,因此不能直接通過`loop.create_task(my_coroutine(i))`訪問原線程的loop對象
            # loop.create_task(my_coroutine(i)) # is wrong -> use asyncio.run_coroutine_threadsafe instead
            asyncio.run_coroutine_threadsafe(my_coroutine(i), loop)
            print(f"separated thread: submit coroutine {i} to loop, and going to sleep 2")
            time.sleep(2)  # 阻塞 2 秒提交一個新的協(xié)程
            i += 1
    except Exception as e:
        print(f"Caught exception: {e}")
        loop.stop()


loop = asyncio.new_event_loop()
# 開啟一個獨立線程,向loop提交task,線程立刻執(zhí)行
loop.run_in_executor(None, submit_coroutines_blocking, loop)
loop.run_forever()

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內(nèi)容