咱們都知道對於I/O相關的程序來講,異步編程能夠大幅度的提升系統的吞吐量,由於在某個I/O操做的讀寫過程當中,系統能夠先去處理其它的操做(一般是其它的I/O操做),那麼Python中是如何實現異步編程的呢?javascript
簡單的回答是Python經過協程(coroutine)來實現異步編程。那究竟啥是協程呢?這將是一個很長的故事。
故事要從yield開始提及(已經熟悉yield的讀者能夠跳過這一節)。html
yield是用來生成一個生成器的(Generator), 生成器又是什麼呢?這又是一個長長的story,因此此次我建議您移步到這裏:
徹底理解Python迭代對象、迭代器、生成器,而關於yield是怎麼回事,建議看這裏:[翻譯]PYTHON中YIELD的解釋java
好了,如今假設你已經明白了yield和generator的概念了,請原諒我這種不負責任的說法可是這真的是一個很長的story啊!python
總的來講,yield至關於return,它將相應的值返回給調用next()或者send()的調用者,從而交出了cpu使用權,而當調用者再調用next()或者send()時,又會返回到yield中斷的地方,若是send有參數,又會將參數返回給yield賦值的變量,若是沒有就跟next()同樣賦值爲None。可是這裏會遇到一個問題,就是嵌套使用generator時外層的generator須要寫大量代碼,看以下示例:git
注意如下代碼均在Python3.6上運行調試github
#!/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 fromexpress
使用yield from有兩個好處,編程
咱們能夠將上邊的代碼修改以下:小程序
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()
執行結果以下:bash
do something before coroutine start 1 2 3 4 5 6 7 8 9 10
這裏inner_generator()中執行的代碼片斷咱們實際就能夠認爲是協程,因此總的來講邏輯圖以下:
接下來咱們就看下究竟協程是啥樣子
協程的概念應該是從進程和線程演變而來的,他們都是獨立的執行一段代碼,可是不一樣是線程比進程要輕量級,協程比線程還要輕量級。多線程在同一個進程中執行,而協程一般也是在一個線程當中執行。它們的關係圖以下:
咱們都知道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語法,從而有了原生協程的概念。
在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能夠參考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的組合創造的驚人的性能,感興趣的同窗能夠找下相關文章,也許後續我會再專門就此話題寫一篇文章,歡迎交流!