什麼是非同步程式設計?
注:本文說的同時是一個直觀上感覺的概念,只是為了簡化,不是嚴格意義上的同一時刻。
同步程式碼(synchrnous code)我們都很熟悉,就是執行完一個步驟再執行下一個。要在同步程式碼裡面實現"同時"執行多個任務,最簡單也是最直觀地方式就是執行多個 threads 或者多個 processes。這個層次的『同時執行』多個任務,是作業系統協助完成的。 也就是作業系統的任務排程系統來決定什麼時候執行這個任務,什麼時候切換任務,你自己,作為一個應用層的程式設計師,是沒辦法進行干預的。
我相信你也已經聽說了什麼關於 thread 和 process 的抱怨:process 太重,thread 又要牽涉到很多頭條的鎖問題。尤其是對於一個 Python 開發者來說,由於GIL(全域性直譯器鎖)的存在,多執行緒無法真正使用多核,如果你用多執行緒來執行計算型任務,速度會更慢。
非同步程式設計與之不同的是,值使用一個程式,不使用 threads,但是也能實現"同時"執行多個任務(這裡的任務其實就是函式)。
這些函式有一個非常 nice 的 feature:必要的可以暫停,把執行的權利交給其他函式。等到時機恰當,又可以恢復之前的狀態繼續執行。這聽上去是不是有點像程式呢?可以暫停,可以恢復執行。只不過程式的排程是作業系統完成的,這些函式的排程是程式自己(或者說程式設計師你自己)完成的。這也就意味著這將省去了很多計算機的資源,因為程式的排程必然需要大量 syscall,而 syscall 是很昂貴的。
非同步程式設計注意事項
有一點是需要格外注意的,非同步程式碼裡面不能用任何會 block 的函式!也就是說你的程式碼裡面不應該出現下面這些:
- time.sleep()
- 會阻塞的 socket
- requests.get()
- 會阻塞的資料庫呼叫
為什麼呢?在用 thread 或 process 的時候,程式碼阻塞了有作業系統來幫你排程,所以才不會出現『一處阻塞,處處傻等』的情況。
但是現在,對於作業系統來說,你的程式就是一個普通的程式,他並不知道你分了哪些不同的任務,一切都要靠你自己了。如果你的程式碼裡出現了阻塞的呼叫,那麼其他部分確實就是傻傻地等著。(等下判斷一下這會不會出錯)。
小試Python asyncio
Python 版本支援情況
- asyncio 模組在 Python3.4 時釋出。
- async 和 await 關鍵字最早在 Python3.5中引入。
- Python3.3之前不支援。
開始動手敲程式碼
同步版本
就是一個簡單的訪問百度首頁100次,然後列印狀態碼。
import time
import requests
def visit_sync():
start = time.time()
for _ in range(100):
r = requests.get(URL)
print(r.status_code)
end = time.time()
print("visit_sync tasks %.2f seconds" % (end - start))
if __name__ == '__main__':
visit_sync()
複製程式碼
執行一下,發現使用了6.64秒。
非同步版本
import time
import asyncio
import aiohttp
async def fetch_async(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
status_code = resp.status
print(status_code)
async def visit_async():
start = time.time()
tasks = []
for _ in range(100):
tasks.append(fetch_async(URL))
await asyncio.gather(*tasks)
end = time.time()
print("visit_async tasks %.2f seconds" % (end - start))
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(visit_async())
複製程式碼
有幾點說明一下:
- 網路訪問的部分變了,前面用的是 requests.get(),這裡用的是 aiohttp(不是標準庫需要自己安裝)。
- 呼叫函式的方式變了,前面通過
visit_sync()
就可以直接執行,非同步程式碼中不能直接visit_async()
,這會提示你一個 warning:
如果列印一下visit_async()
返回值的型別可以看到,這是一個coroutine(協程)。
正常的姿勢是呼叫await visit_async()
,就想程式碼中await asyncio.gather(*tasks)
一樣。但是比較麻煩的一點是await
只有在以關鍵字async
定義的函式裡面使用,而我們的if __name__ == "__main__"
裡面沒有函式,所以可以把這個 coroutine
傳給一個 eventloop。
loop = asyncio.get_event_loop()
loop.run_until_complete(visit_async())
複製程式碼
執行之後發現,耗時0.34秒,效率提升20多倍。(關於如何有逼格地分析非同步效率,可以參考前面寫過的一篇文章。)
總結一下
事實上,這篇文章已經引出了非同步程式設計中一個重要的概念:協程。『非同步程式設計101』系列文章後面還會花很多篇幅說一說一下協程。
協程"同時"執行多個任務的基礎是函式可以暫停(後面我們會講到這一點是如何實現的,Python 中是通過 yield)。上面的程式碼中使用到了asyncio
的 event_loop,它做的事情,本質上來說就是當函式暫停時,切換到下一個任務,當時機恰當(這個例子中是請求完成了)恢復函式讓他繼續執行(這有點像作業系統了)。
這相比使用多執行緒或多程式,把排程地任務交給作業系統,在效能上有極大的優勢,因為不需要大量的 syscall。同時又解決了多執行緒資料共享帶來的鎖的問題。而且作為一個應用程式開發者,你應該是要比作業系統更懂,哪些時候進行任務切換。
我個人覺得,新時代的程式設計師,有兩點技能是非常重要的:非同步程式設計的能力和利用多核系統的能力。
覺得不錯點個 star?
我的公眾號:全棧不存在的