之前我們講過了 Python 的 Coroutine,提到了我們用的是 asyncio library。不同於 Coroutine 那一篇,這一篇我們注重於原理的理解。
我們知道,multi-threading 能夠使得效率及 CPU 使用率大大提高,那為什麼我們還需要 Asyncio (Coroutine) 呢?
- 因為在 multi-threading 下,程式碼容易被作業系統打斷,因此可能會出現 race condition。
- 另外,context switch 會造成性能損耗。如果 I/O 過多,不停的 context switch 會損失很多性能。
於是就有了 Asyncio。
Asyncio 原理
Asyncio 和 Python 的程式一樣,實際上是 single thread 的,不過可以不停地切換。只要拿到 GIL 就可以進行任務。這裡的任務是一種特殊的 future object,並且被 Event loop 所控制。
我們可以把任務分成兩個狀態 - 預備狀態和完成狀態。預備狀態指的是任務目前空閒,隨時可以執行;而等待狀態則是任務已經執行,但是被掛著等待某個操作完成,例如 I/O。
Event loop 有兩個 job list,分別對應兩種狀態,並且選取一個預備狀態的任務執行它,一直到它被交還給 Event loop。當任務被交還給 Event loop 時,Event loop 會根據其是否完成,把任務放進預備或完成狀態的 list:
- 如果完成,則放進預備狀態;
- 如果未完成,則放進等待狀態。
然後再遍歷等待狀態 list,看他們是否完成。如果完成再放進預備狀態。如此週而復始,直到所有任務完成。由於 Asyncio 的任務不會被外部因素打斷,所以 Asyncio 裡面的操作不會出現 race condition 的問題。
例子程式碼
讓我們複習一下這段程式碼:
1 | import asyncio |
在這段程式碼中,async/await
是 asyncio 的一種寫法,代表這個函數或這行程式碼是 non-blocking 的。如果這裡很耗時,那我們就把控制權交還給 Event loop,放進等待狀態 list。另外,asyncio.run()
是 Python 3.7 才有的,在舊版本是這樣寫:
1 | import asyncio |
Asyncio 有缺點嗎?
當然有。我們知道 Asyncio 的 event loop 藉由 async/await 去把 job 控制權交還給 Event loop。所以相應的第三方 library 也要做調整才能完美地利用 Coroutine 提升性能。例如,著名的 requests
library 就沒有支持 Asyncio,而相同功能的 aiohttp
則有兼容 Asyncio。
multi-processing、multi-threading 還是 coroutine?
總結來說:
- 如果是 CPU heavy 的任務,使用 multi-processing。
- 如果 I/O 慢或太多,使用 Coroutine。
- 反之,如果 I/O 快,使用 multi-threading。
總結
這篇文章帶大家過了 Asyncio Event loop 的原理:
- Asyncio 是 single thread 的,但是透過 Event loop,併發地執行不同任務,在程式端享有自主控制權。
- 由於打斷是自己控制的,不會出現 race condition 的問題。在 I/O heavy 的情況下,比 multi-threading 的效率更好,因為不用 context switch,且能開啟的任務數量更多。
- 不過 Asyncio 的缺點是,需要第三方 library 的支持。