注:本文說的同時是一個直觀上感受的概念,只是爲了簡化,不是嚴格意義上的同一時刻。python
同步代碼(synchrnous code)咱們都很熟悉,就是運行完一個步驟再運行下一個。要在同步代碼裏面實現"同時"運行多個任務,最簡單也是最直觀地方式就是運行多個 threads 或者多個 processes。這個層次的『同時運行』多個任務,是操做系統協助完成的。 也就是操做系統的任務調度系統來決定何時運行這個任務,何時切換任務,你本身,做爲一個應用層的程序員,是沒辦法進行干預的。程序員
我相信你也已經據說了什麼關於 thread 和 process 的抱怨:process 過重,thread 又要牽涉到不少頭條的鎖問題。尤爲是對於一個 Python 開發者來講,因爲GIL(全局解釋器鎖)的存在,多線程沒法真正使用多核,若是你用多線程來運行計算型任務,速度會更慢。數據庫
異步編程與之不一樣的是,值使用一個進程,不使用 threads,可是也能實現"同時"運行多個任務(這裏的任務其實就是函數)。編程
這些函數有一個很是 nice 的 feature:必要的時候能夠暫停,把運行的權利交給其餘函數。等到時機恰當,又能夠恢復以前的狀態繼續運行。這聽上去是否是有點像進程呢?能夠暫停,能夠恢復運行。只不過進程的調度是操做系統完成的,這些函數的調度是進程本身(或者說程序員你本身)完成的。這也就意味着這將省去了不少計算機的資源,由於進程的調度必然須要大量 syscall,而 syscall 是很昂貴的。bash
有一點是須要格外注意的,異步代碼裏面不該該用會 block 的函數!也就是說你的代碼裏面不該該出現下面這些:網絡
爲何呢?在用 thread 或 process 的時候,代碼阻塞了有操做系統來幫你調度,因此纔不會出現『一處阻塞,到處傻等』的狀況。session
可是如今,對於操做系統來講,你的進程就是一個普通的進程,他並不知道你分了哪些不一樣的任務,一切都要靠你本身了。若是你的代碼裏出現了阻塞的調用,那麼其餘部分確實就是傻傻地等着。(等下判斷一下這會不會出錯)。多線程
就是一個簡單的訪問百度首頁100次,而後打印狀態碼。app
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())
複製代碼
有幾點說明一下:
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?
個人公衆號:全棧不存在的