Python之深入理解asyncio(三)

前言

這篇文章是《深入理解asyncio》的第三篇,主要包含回調,多線程和在asyncio中執行同步代碼。

成功回調

可以給Task(Future)添加回調函數,等Task完成後就會自動調用這個(些)回調:



可以看到在任務完成後執行了callback函數。我這裏順便解釋一個問題,不知道有沒有人注意到。

爲什麼之前一直推薦大家用 asyncio.create_task,但是很多例子卻用了 loop.create_task?

這是因爲在IPython裏面支持方便的使用await執行協程,但如果直接用 asyncio.create_task會報「no running event loop」:

Eventloop是在單進程裏面的單線程中的,在IPython裏面await的時候會把協程註冊到一個線程的Eventloop上,但是REPL環境是另外一個線程,不是一個線程,所以會提示這個錯誤,即便 asyncio.events._set_running_loop(loop)設置了loop,任務可以創建倒是不能await:因爲task是在線程X的Eventloop上註冊的,但是await時卻到線程Y的Eventloop上去執行。這部分是C實現的,可以看延伸閱讀鏈接1。

所以現在你就會看到很多 loop.create_task的代碼片段,別擔心,在代碼項目裏面都是用 asyncio.create_task的,如果你非常想要在IPython裏面使用 asyncio.create_task也不是沒有辦法,可以這樣做:

這樣就可以啦。我解釋下爲什麼:

IPython裏面能運行await是由於loop_runner函數,這個函數能運行協程(延伸閱讀鏈接2),默認的效果大概是 asyncio.get_event_loop().run_until_complete(coro)。爲了讓 asyncio.create_task正常運行我定義了新的loop_runner
通過autoawait這個magic函數就可以重新設置loop_runner
上面的報錯是「no running event loop」,所以通過 events._set_running_loop(loop)設置一個正在運行的loop,但是在默認的loop_runner中也無法運行,會報「Cannot run the event loop while another loop is running」,所以重置await裏面那個running的loop,運行結束再設置回去。
如果你覺得有必要,可以在IPython配置文件中設置這個loop_runner到 c.InteractiveShell.loop_runner上~

好,我們說回來, add_done_callback方法也是支持參數的,但是需要用到 functools.partial:

調度回調

asyncio提供了3個按需回調的方法,都在Eventloop對象上,而且也支持參數:

call_soon

在下一次事件循環中被回調,回調是按其註冊順序被調用的:

這個例子輸出的比較複雜,我挨個分析:

call_soon可以用來設置任務的結果: 用 mark_done
通過2個print可以感受到 call_soon支持參數。
最重要的就是輸出部分了,首先fut.done()的結果是False,因爲還沒到下個事件循環,sleep(0)就可以切到下次循環,這樣就會調用三個 call_soon回調,最後再看fut.done()的結果就是True,而且 fut.result()可以拿到之前在 mark_done設置的值了
call_later

安排回調在給定的時間(單位秒)後執行:

這次要注意3個回調的延遲時間時間要<=sleep的,要不然還沒來的回調程序就結束了

call_at

安排回調在給定的時間執行,注意這個時間要基於 loop.time() 獲取當前時間

同步代碼

前面的代碼都是異步的,就如sleep,需要用 asyncio.sleep而不是阻塞的 time.sleep,如果有同步邏輯,怎麼;利用asyncio實現併發呢?答案是用 run_in_executor。在一開始我說過開發者創建 Future 對象情況很少,主要是用 run_in_executor,就是讓同步函數在一個執行器( executor)裏面運行:

可以看到用 asyncio.gather可以把同步函數邏輯轉化成一個協程,且實現了併發。這裏要注意細節,就是函數a是普通函數,不能寫成協程,下面的定義是錯誤的,不能實現併發:
因爲 a 裏面沒有異步代碼,就不要用 asyncdef來定義。需要把這種邏輯用 loop.run_in_executor封裝到協程:

大家理解了吧?

loop.run_in_executor(None,a)這裏面第一個參數是要傳遞 concurrent.futures.Executor實例的,傳遞None會選擇默認的executor:

當然我們還可以用進程池,這次換個常用的文件讀寫例子,並且用:


多線程

上一個小節用的 run_in_executor就如它方法的名字所示,把協程放到了一個執行器裏面,可以在一個線程池,也可以在一個進程池。另外還可以使用 run_coroutine_threadsafe在其他線程執行協程(這是線程安全的):

這裏面有幾個細節要注意:

協程應該從另一個線程中調用,而非事件循環運行所在線程,所以用 asyncio.new_event_loop()新建一個事件循環
在執行協程前要確保新創建的事件循環是運行着的,所以需要用 start_loop之類的方式啓動循環
接着就可以用 asyncio.run_coroutine_threadsafe執行協程a了,它返回了一個Future對象
可以通過輸出感受到future一開始是pending的,因爲協程a裏面會sleep 1秒才返回結果
用 future.result(timeout=2)就可以獲得結果,設置timeout的值要大於a協程執行時間,要不然會拋出TimeoutError
一開始我們創建的新的事件循環跑在一個線程裏面,由於 loop.run_forever會阻塞程序關閉,所以需要結束時殺掉線程,所以用 call_soon_threadsafe回調函數 shutdown去停止事件循環
這裏再說一下 call_soon_threadsafe,看名字就知道它是線程安全版本的 call_soon,其實就是在另外一個線程裏面調度回調。BTW, 其實 asyncio.run_coroutine_threadsafe底層也是用的它。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章