最新Python異步編程詳解

咱們都知道對於I/O相關的程序來講,異步編程能夠大幅度的提升系統的吞吐量,由於在某個I/O操做的讀寫過程當中,系統能夠先去處理其它的操做(一般是其它的I/O操做),那麼Python中是如何實現異步編程的呢?javascript

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

yield

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

使用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() 

執行結果以下:bash

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 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索