最新Python異步編程詳解


我們都知道對於I/O相關的程序來說,異步編程可以大幅度的提高系統的吞吐量,因為在某個I/O操作的讀寫過程中,系統可以先去處理其它的操作(通常是其它的I/O操作),那么Python中是如何實現異步編程的呢?

簡單的回答是Python通過協程(coroutine)來實現異步編程。那究竟啥是協程呢?這將是一個很長的故事。
故事要從yield開始說起(已經熟悉yield的讀者可以跳過這一節)。

yield

yield是用來生成一個生成器的(Generator), 生成器又是什么呢?這又是一個長長的story,所以這次我建議您移步到這里:
完全理解Python迭代對象、迭代器、生成器,而關於yield是怎么回事,建議看這里:[翻譯]PYTHON中YIELD的解釋

好了,現在假設你已經明白了yield和generator的概念了,請原諒我這種不負責任的說法但是這真的是一個很長的story啊!

總的來說,yield相當於return,它將相應的值返回給調用next()或者send()的調用者,從而交出了cpu使用權,而當調用者再調用next()或者send()時,又會返回到yield中斷的地方,如果send有參數,又會將參數返回給yield賦值的變量,如果沒有就跟next()一樣賦值為None。但是這里會遇到一個問題,就是嵌套使用generator時外層的generator需要寫大量代碼,看如下示例:

注意以下代碼均在Python3.6上運行調試

#!/usr/bin/env python # encoding:utf-8 def inner_generator(): i = 0 while True: i = yield i if i > 10: raise StopIteration def outer_generator(): print("do something before yield") from_inner = 0 from_outer = 1 g = inner_generator() g.send(None) while 1: try: from_inner = g.send(from_outer) from_outer = yield from_inner except StopIteration: break def main(): g = outer_generator() g.send(None) i = 0 while 1: try: i = g.send(i + 1) print(i) except StopIteration: break if __name__ == '__main__': main() 

為了簡化,在Python3.3中引入了yield from

yield from

使用yield from有兩個好處,

  1. 可以將main中send的參數一直返回給最里層的generator,
  2. 同時我們也不需要再使用while循環和send (), next()來進行迭代。

我們可以將上邊的代碼修改如下:

def inner_generator(): i = 0 while True: i = yield i if i > 10: raise StopIteration def outer_generator(): print("do something before coroutine start") yield from inner_generator() def main(): g = outer_generator() g.send(None) i = 0 while 1: try: i = g.send(i + 1) print(i) except StopIteration: break if __name__ == '__main__': main() 

執行結果如下:

do something before coroutine start 1 2 3 4 5 6 7 8 9 10 

這里inner_generator()中執行的代碼片段我們實際就可以認為是協程,所以總的來說邏輯圖如下:

 
coroutine and wrapper

接下來我們就看下究竟協程是啥樣子

協程coroutine

協程的概念應該是從進程和線程演變而來的,他們都是獨立的執行一段代碼,但是不同是線程比進程要輕量級,協程比線程還要輕量級。多線程在同一個進程中執行,而協程通常也是在一個線程當中執行。它們的關系圖如下:

 
process, thread and coroutine

我們都知道Python由於GIL(Global Interpreter Lock)原因,其線程效率並不高,並且在*nix系統中,創建線程的開銷並不比進程小,因此在並發操作時,多線程的效率還是受到了很大制約的。所以后來人們發現通過yield來中斷代碼片段的執行,同時交出了cpu的使用權,於是協程的概念產生了。在Python3.4正式引入了協程的概念,代碼示例如下:

import asyncio # Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html. @asyncio.coroutine def countdown(number, n): while n > 0: print('T-minus', n, '({})'.format(number)) yield from asyncio.sleep(1) n -= 1 loop = asyncio.get_event_loop() tasks = [ asyncio.ensure_future(countdown("A", 2)), asyncio.ensure_future(countdown("B", 3))] loop.run_until_complete(asyncio.wait(tasks)) loop.close() 

示例顯示了在Python3.4引入兩個重要概念協程事件循環
通過修飾符@asyncio.coroutine定義了一個協程,而通過event loop來執行tasks中所有的協程任務。之后在Python3.5引入了新的async & await語法,從而有了原生協程的概念。

async & await

在Python3.5中,引入了aync&await 語法結構,通過"aync def"可以定義一個協程代碼片段,作用類似於Python3.4中的@asyncio.coroutine修飾符,而await則相當於"yield from"。

先來看一段代碼,這個是我剛開始使用async&await語法時,寫的一段小程序。

#!/usr/bin/env python # encoding:utf-8 import asyncio import requests import time async def wait_download(url): response = await requests.get(url) print("get {} response complete.".format(url)) async def main(): start = time.time() await asyncio.wait([ wait_download("http://www.163.com"), wait_download("http://www.mi.com"), wait_download("http://www.google.com")]) end = time.time() print("Complete in {} seconds".format(end - start)) loop = asyncio.get_event_loop() loop.run_until_complete(main()) 

這里會收到這樣的報錯:

Task exception was never retrieved
future: <Task finished coro=<wait_download() done, defined at asynctest.py:9> exception=TypeError("object Response can't be used in 'await' expression",)> Traceback (most recent call last): File "asynctest.py", line 10, in wait_download data = await requests.get(url) TypeError: object Response can't be used in 'await' expression 

這是由於requests.get()函數返回的Response對象不能用於await表達式,可是如果不能用於await,還怎么樣來實現異步呢?
原來Python的await表達式是類似於"yield from"的東西,但是await會去做參數檢查,它要求await表達式中的對象必須是awaitable的,那啥是awaitable呢? awaitable對象必須滿足如下條件中其中之一:

  • A native coroutine object returned from a native coroutine function .

    原生協程對象

  • A generator-based coroutine object returned from a function decorated with types.coroutine() .

    types.coroutine()修飾的基於生成器的協程對象,注意不是Python3.4中asyncio.coroutine

  • An object with an await method returning an iterator.

    實現了await method,並在其中返回了iterator的對象

根據這些條件定義,我們可以修改代碼如下:

#!/usr/bin/env python # encoding:utf-8 import asyncio import requests import time async def download(url): # 通過async def定義的函數是原生的協程對象 print("get %s" % url) response = requests.get(url) print(response.status_code) async def wait_download(url): await download(url) # 這里download(url)就是一個原生的協程對象 print("get {} data complete.".format(url)) async def main(): start = time.time() await asyncio.wait([ wait_download("http://www.163.com"), wait_download("http://www.mi.com"), wait_download("http://www.baidu.com")]) end = time.time() print("Complete in {} seconds".format(end - start)) loop = asyncio.get_event_loop() loop.run_until_complete(main()) 

至此,程序可以運行,不過仍然有一個問題就是它並沒有真正地異步執行 (這里要感謝網友荊棘花王朝,是Ta指出的這個問題)
看一下運行結果:

get http://www.163.com 200 get http://www.163.com data complete. get http://www.baidu.com 200 get http://www.baidu.com data complete. get http://www.mi.com 200 get http://www.mi.com data complete. Complete in 0.49027466773986816 seconds 

會發現程序始終是同步執行的,這就說明僅僅是把涉及I/O操作的代碼封裝到async當中是不能實現異步執行的。必須使用支持異步操作的非阻塞代碼才能實現真正的異步。目前支持非阻塞異步I/O的庫是aiohttp

#!/usr/bin/env python # encoding:utf-8 import asyncio import aiohttp import time async def download(url): # 通過async def定義的函數是原生的協程對象 print("get: %s" % url) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: print(resp.status) # response = await resp.read() # 此處的封裝不再需要 # async def wait_download(url): # await download(url) # 這里download(url)就是一個原生的協程對象 # print("get {} data complete.".format(url)) async def main(): start = time.time() await asyncio.wait([ download("http://www.163.com"), download("http://www.mi.com"), download("http://www.baidu.com")]) end = time.time() print("Complete in {} seconds".format(end - start)) loop = asyncio.get_event_loop() loop.run_until_complete(main()) 

再看一下測試結果:

get: http://www.mi.com get: http://www.163.com get: http://www.baidu.com 200 200 200 Complete in 0.27292490005493164 seconds 

可以看出這次是真正的異步了。
好了現在一個真正的實現了異步編程的小程序終於誕生了。
而目前更牛逼的異步是使用uvloop或者pyuv,這兩個最新的Python庫都是libuv實現的,可以提供更加高效的event loop。

uvloop和pyuv

關於uvloop可以參考uvloop
pyuv可以參考這里pyuv

pyuv實現了Python2.x和3.x,但是該項目在github上已經許久沒有更新了,不知道是否還有人在維護。
uvloop只實現了3.x, 但是該項目在github上始終活躍。

它們的使用也非常簡單,以uvloop為例,只需要添加以下代碼就可以了

import asyncio import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 

關於Python異步編程到這里就告一段落了,而引出這篇文章的引子實際是關於網上有關Sanic和uvloop的組合創造的驚人的性能,感興趣的同學可以找下相關文章,也許后續我會再專門就此話題寫一篇文章,歡迎交流!



作者:geekpy
鏈接:https://www.jianshu.com/p/b036e6e97c18
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM