# AsyncIO in Python
###### tags: `python` `MCL Notebook` `async`
### 參考文件
* [[Medium] aysnc await 學習筆記](https://medium.com/%E9%AB%92%E6%A1%B6%E5%AD%90/aysnc-await-%E6%95%99%E5%AD%B8%E7%AD%86%E8%A8%98-debabdb9db0e)
* [IT 邦幫忙] python的asyncio模組
* [(一):異步執行的好處](https://ithelp.ithome.com.tw/articles/10199385)
* [(二):異步程式設計基本概念](https://ithelp.ithome.com.tw/articles/10199403)
* [(三):建立Event Loop和定義協程](https://ithelp.ithome.com.tw/articles/10199408)
* [[Taiker] Speed Up Your Python Program With Concurrency](https://blog.taiker.space/python-speed-up-your-python-program-with-concurrency/)
* [Go Concurrency Patterns](https://talks.golang.org/2012/concurrency.slide#7)
## 同步與非同步
一般來說,Python 在對任一行程式進行請求的時候,都等到程式回應之後在進行下一行程式,也就是所謂的 IO 阻塞式的語言。
而非同步的差別在於執行過程不會等待 IO 回應,而是繼續執行下面的程式碼,讓 IO 與後續流程作為**事件 (event)** 形式,並透過**輪詢 (polling)** 與**回調 (callback)** 觸發執行後續程式碼。
最為經典的非同步語言為 JavaScript,也有其他語言透過套件等其他形式去得到非同步的效果,如 ROS,以及本篇著重的對象: Python 的 `asyncio`.
> 非同步程式另一個範例是 Non-blocking sockets (select). 同樣也是透過事件、輪詢與回調進行多端點非阻塞式連線。
直白一點講,故事是這樣的:
1. (同步) 「小明到了便當店點了雞腿便當,等到雞腿便當拿到之後再到飲料店點珍奶。」
2. **(非同步)** 「小明到了便當店點了雞腿便當,在等待的過程中先到飲料店點珍奶,之後再看哪邊先做完就先去拿已經做完的餐點。」
基本上這就是兩者最大的不同。
| Type | Illustration |
| ------------------------- | ------------------------------------------------------------------------------------------- |
| Synchronous 同步 | ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_aa6623c76f9356e90020570f18cd2ba7.png) |
| Multi-processing 多處理器 | ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_79f3d9652ee33c33d5a25bd1e00dcfa5.png) |
| Multi-threading 多執行緒 | ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_f3038ba07be2728c3150e66e735c755c.png) |
| Asynchronous 非同步 | ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_e471e90a794e2cde78b635436762f7fd.png) |
> 上表中除第一項,都是並行的可用方法,而挑選適合的方案,重點在於理解自身程式所遇到的瓶頸是 CPU bound (計算瓶頸) 還是 IO bound (等待瓶頸);若前者可考慮套用 Multi-processing 處理,後者可考慮套用 Multi-threading 或是 Async 處理。
> [name=Taiker. [Speed Up Your Python Program With Concurrency](https://blog.taiker.space/python-speed-up-your-python-program-with-concurrency/)]
### 並行 (Concurrent) 與平行 (Parallel)
稍微討論一下這兩個很類似的名詞。並行 (Concurrent) 含意是一種「得以在同時間有兩個以上的計算在處理」的程式語言或是各種演算法;而平行 (Parallel) 是一種實現並行的一種模式。
一個衝突的案例是:當你只有一個處理器時,你依然可以擁有並行運算,但你無法透過平行實現。
> ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_78e809f8404ea64a51416c556b17e9ec.png)
> [name=Andrew Gerrand. [Go Concurrency Patterns](https://talks.golang.org/2012/concurrency.slide#7).]
### 非同步的哲學觀:事件與回調
![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_88a6cd6ad01300d9912a63dd2ae2d811.png)
> [name=luminousmen. [Asynchronous programming. Cooperative multitasking](https://luminousmen.com/post/asynchronous-programming-cooperative-multitasking)]
> 「小明到了便當店點了雞腿便當,在等待的過程中先到飲料店點珍奶,之後再看哪邊先做完就先去拿已經做完的餐點。」
回顧一下上面提到的故事:
1. 「點雞腿便當」跟「點珍奶」就是兩個任務,而指派出去的時候就會進入事件循環 (Event Loop)
2. 二事件接著就會進入 IO 等待階段 (等店家把便當跟珍奶做好),而小明就會在事件循環 (Event Loop) 中一直輪詢 (Pooling) 以查看每個任務的狀態
3. 當有其中一個食物做好的時候 (Callback),就會觸發後續行為 (取餐)。
在 Python 當中會使用 `asyncio` 套件來實現上面的流程,並且會使用 `async` 和 `await` 兩關鍵字去撰寫相關程式碼。
## 範例程式碼
### 案例一:`sleep`
我們透過 Python 的 `asyncio` 進行多事件睡眠以模擬 IO bound 狀態下的效果:
```python
import time
import asyncio
start_time = time.perf_counter() # such as time(), but more precise
loop = asyncio.get_event_loop()
async def pseudo_io(idx):
t = time.perf_counter()
print(idx, ': send a request at', t - start_time, 'seconds')
await asyncio.sleep(1) # do not use time.sleep(1) as its blocking mode
t = time.perf_counter()
print(idx, ': receive a response at ', t - start_time, 'seconds')
if __name__ == '__main__':
tasks = []
for i in range(10):
task = loop.create_task(pseudo_io(i + 1))
tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
```
你會得到類似以下的結果:
```
1 : send a request at 0.0005315999999999932 seconds
2 : send a request at 0.0006520999999999888 seconds
3 : send a request at 0.0009392999999999901 seconds
4 : send a request at 0.0010235999999999995 seconds
5 : send a request at 0.0011018 seconds
6 : send a request at 0.0011733999999999911 seconds
7 : send a request at 0.001246199999999989 seconds
8 : send a request at 0.0013195999999999902 seconds
9 : send a request at 0.0013951999999999992 seconds
10 : send a request at 0.0014667999999999903 seconds
1 : receive a response at 1.0014887 seconds
3 : receive a response at 1.0016416 seconds
7 : receive a response at 1.0019822 seconds
10 : receive a response at 1.0023405 seconds
9 : receive a response at 1.0026532 seconds
6 : receive a response at 1.0029613 seconds
8 : receive a response at 1.0087264 seconds
5 : receive a response at 1.0091539999999999 seconds
2 : receive a response at 1.0094932 seconds
4 : receive a response at 1.0099035 seconds
```
### 案例二:`requests`
網頁請求是一個經典的 IO bound 問題,他也很適合透過非同步手法處理之。
```python
import time
import requests
import asyncio
start_time = time.perf_counter()
loop = asyncio.get_event_loop()
async def send_request(idx, url):
t = time.perf_counter()
print(idx, ': send a request at', t - start_time, 'seconds')
_ = await loop.run_in_executor(None, requests.get, url)
t = time.perf_counter()
print(idx, ': receive a response at ', t - start_time, 'seconds')
if __name__ == '__main__':
tasks = []
url = 'https://www.google.com'
for i in range(10):
task = loop.create_task(send_request(i + 1, url))
tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
```
類似結果如下:
```
1 : send a request at 0.0 seconds
2 : send a request at 0.002009868621826172 seconds
3 : send a request at 0.002009868621826172 seconds
4 : send a request at 0.003000020980834961 seconds
5 : send a request at 0.003000020980834961 seconds
6 : send a request at 0.003000020980834961 seconds
7 : send a request at 0.004000186920166016 seconds
8 : send a request at 0.0060007572174072266 seconds
9 : send a request at 0.00900578498840332 seconds
10 : send a request at 0.012999773025512695 seconds
1 : receive a response at 0.08250999450683594 seconds
5 : receive a response at 0.0855417251586914 seconds
8 : receive a response at 0.08650994300842285 seconds
6 : receive a response at 0.08753800392150879 seconds
7 : receive a response at 0.08850979804992676 seconds
10 : receive a response at 0.08951115608215332 seconds
2 : receive a response at 0.34174418449401855 seconds
4 : receive a response at 0.34679532051086426 seconds
9 : receive a response at 0.3497951030731201 seconds
3 : receive a response at 0.35082435607910156 seconds
```