[Python進階]Asyncio的Event loop

之前我們講過了 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import asyncio
async def loading(sec):
print('loading... needs {} secs to load'.format(sec))
await asyncio.sleep(sec)
print('OK {}'.format(sec))

async def main(secs):
tasks = [asyncio.create_task(loading(sec)) for sec in secs]
await asyncio.gather(*tasks)

asyncio.run(main([5, 3]))
# Output:
# loading... needs 5 secs to load
# loading... needs 3 secs to load
# OK 3
# OK 5

在這段程式碼中,async/await 是 asyncio 的一種寫法,代表這個函數或這行程式碼是 non-blocking 的。如果這裡很耗時,那我們就把控制權交還給 Event loop,放進等待狀態 list。另外,asyncio.run() 是 Python 3.7 才有的,在舊版本是這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import asyncio
loop = asyncio.get_event_loop()
async def loading(sec):
print('loading... needs {} secs to load'.format(sec))
await asyncio.sleep(sec)
print('OK {}'.format(sec))

def main(secs):
loop.run_until_complete(
asyncio.wait(
[asyncio.ensure_future(loading(sec)) for sec in secs]
)
)
main([5, 3])
loop.close()
# Output:
# loading... needs 5 secs to load
# loading... needs 3 secs to load
# OK 3
# OK 5

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 的支持。