異步編程 101: 是什麼、小試Python asyncio

什麼是異步編程?

注:本文說的同時是一個直觀上感受的概念,只是爲了簡化,不是嚴格意義上的同一時刻。python

同步代碼(synchrnous code)咱們都很熟悉,就是運行完一個步驟再運行下一個。要在同步代碼裏面實現"同時"運行多個任務,最簡單也是最直觀地方式就是運行多個 threads 或者多個 processes。這個層次的『同時運行』多個任務,是操做系統協助完成的。 也就是操做系統的任務調度系統來決定何時運行這個任務,何時切換任務,你本身,做爲一個應用層的程序員,是沒辦法進行干預的。程序員

我相信你也已經據說了什麼關於 thread 和 process 的抱怨:process 過重,thread 又要牽涉到不少頭條的鎖問題。尤爲是對於一個 Python 開發者來講,因爲GIL(全局解釋器鎖)的存在,多線程沒法真正使用多核,若是你用多線程來運行計算型任務,速度會更慢。數據庫

異步編程與之不一樣的是,值使用一個進程,不使用 threads,可是也能實現"同時"運行多個任務(這裏的任務其實就是函數)。編程

這些函數有一個很是 nice 的 feature:必要的時候能夠暫停,把運行的權利交給其餘函數。等到時機恰當,又能夠恢復以前的狀態繼續運行。這聽上去是否是有點像進程呢?能夠暫停,能夠恢復運行。只不過進程的調度是操做系統完成的,這些函數的調度是進程本身(或者說程序員你本身)完成的。這也就意味着這將省去了不少計算機的資源,由於進程的調度必然須要大量 syscall,而 syscall 是很昂貴的。bash

異步編程注意事項

有一點是須要格外注意的,異步代碼裏面不該該用會 block 的函數!也就是說你的代碼裏面不該該出現下面這些:網絡

  • time.sleep()
  • 會阻塞的 socket
  • requests.get()
  • 會阻塞的數據庫調用

爲何呢?在用 thread 或 process 的時候,代碼阻塞了有操做系統來幫你調度,因此纔不會出現『一處阻塞,到處傻等』的狀況。session

可是如今,對於操做系統來講,你的進程就是一個普通的進程,他並不知道你分了哪些不一樣的任務,一切都要靠你本身了。若是你的代碼裏出現了阻塞的調用,那麼其餘部分確實就是傻傻地等着。(等下判斷一下這會不會出錯)。多線程

小試Python asyncio

Python 版本支持狀況

  • asyncio 模塊在 Python3.4 時發佈。
  • async 和 await 關鍵字最先在 Python3.5中引入。
  • Python3.3以前不支持。

開始動手敲代碼

同步版本

就是一個簡單的訪問百度首頁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())
複製代碼

有幾點說明一下:

  • 網絡訪問的部分變了,前面用的是 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?

個人公衆號:全棧不存在的

相關文章
相關標籤/搜索