今夕何夕故人不來遲暮連山黛
之前有研究過python協程相關的知識,但一直沒有進行深入探究。平常工作中使用的也還是以python2為主,然而最近的專案需要使用python3協程相關的內容,因此湊出時間學習了一番python3的協程語法。 本篇主要以介紹python3.5的async/await協程語法為主,因為這種語法看上去很彆扭,不容易理解。如果對python協程基礎不是很瞭解,建議可以先看此篇:Python協程。
協程函式(非同步函式)
我們平常使用最多的函式都是同步函式,即不同函式執行是按順序執行的。那麼什麼是非同步函式呢?怎麼建立非同步函式?怎麼在非同步函式之間來回切換執行?不急,請往下看。
建立協程函式
先來看下普通函式:
def test1():
print("1")
print("2")
def test2():
print("3")
print("4")
a = test1()
b = test2()
print(a,type(a))
print(b,type(b))
複製程式碼
執行以上程式碼得到結果:
1
2
3
4
None <class 'NoneType'>
None <class 'NoneType'>
複製程式碼
說明:程式順序執行了test1、test2函式,在呼叫函式的時候就自動進入了函式體,並執行了函式的內容。
然後使用async關鍵詞將普通函式變成協程函式,即非同步函式:
async def test1():
print("1")
print("2")
async def test2():
print("3")
print("4")
print(test1())
print(test2())
複製程式碼
執行以上程式碼得到結果:
<coroutine object test1 at 0x109f4c620>
asyncio_python3_test.py:16: RuntimeWarning: coroutine 'test1' was never awaited
print(test1())
<coroutine object test2 at 0x109f4c620>
asyncio_python3_test.py:17: RuntimeWarning: coroutine 'test2' was never awaited
print(test2())
複製程式碼
說明:忽略結果中的告警,可以看到呼叫函式test1、test2的時候,並沒有進入函式體且執行函式內容,而是返回了一個coroutine(協程物件)。
除了函式外,類的方法也可以使用async關鍵詞將其變成協程方法:
class test:
async def run(self):
print("1")
複製程式碼
執行協程函式
前面我們成功建立了協程函式,並且在呼叫函式的時候返回了一個協程物件,那麼怎麼進入函式體並執行函式內容呢?類似於生成器,可以使用send方法執行函式,修改下前面的程式碼:
async def test1():
print("1")
print("2")
async def test2():
print("3")
print("4")
a = test1()
b = test2()
a.send(None)
b.send(None)
複製程式碼
執行以上程式碼得到以下結果:
1
2
Traceback (most recent call last):
File "asyncio_python3_test.py", line 19, in <module>
a.send(None)
StopIteration
sys:1: RuntimeWarning: coroutine 'test2' was never awaited
複製程式碼
說明:程式先執行了test1協程函式,當test1執行完時報了StopIteration異常,這是協程函式執行完飯回的一個異常,我們可以用try except捕捉,來用判斷協程函式是否執行完畢。
async def test1():
print("1")
print("2")
async def test2():
print("3")
print("4")
a = test1()
b = test2()
try:
a.send(None) # 可以通過呼叫 send 方法,執行協程函式
except StopIteration as e:
print(e.value)
# 協程函式執行結束時會丟擲一個StopIteration 異常,標誌著協程函式執行結束,返回值在value中
pass
try:
b.send(None) # 可以通過呼叫 send 方法,執行協程函式
except StopIteration:
print(e.value)
# 協程函式執行結束時會丟擲一個StopIteration 異常,標誌著協程函式執行結束,返回值在value中
pass
複製程式碼
執行以上程式碼得到以下結果:
1
2
3
4
複製程式碼
說明:程式先執行了test1函式,等到test1函式執行完後再執行test2函式。從執行過程上來看目前協程函式與普通函式沒有區別,並沒有實現非同步函式,那麼如何交叉執行協程函式呢?
交叉執行協程函式(await)
通過以上例子,我們發現定義協程函式可以使用async關鍵詞,執行函式可以使用send方法,那麼如何實現在兩個協程函式間來回切換執行呢?這裡需要使用await關鍵詞,修改一下程式碼:
import asyncio
async def test1():
print("1")
await asyncio.sleep(1) # asyncio.sleep(1)返回的也是一個協程物件
print("2")
async def test2():
print("3")
print("4")
a = test1()
b = test2()
try:
a.send(None) # 可以通過呼叫 send 方法,執行協程函式
except StopIteration:
# 協程函式執行結束時會丟擲一個StopIteration 異常,標誌著協程函式執行結束
pass
try:
b.send(None) # 可以通過呼叫 send 方法,執行協程函式
except StopIteration:
pas
複製程式碼
執行以上函式得到以下結果:
1
3
4
複製程式碼
說明:程式先執行test1協程函式,在執行到await時,test1函式停止了執行(阻塞);接著開始執行test2協程函式,直到test2執行完畢。從結果中,我們可以看到,直到程式執行完畢,test1函式也沒有執行完(沒有執行print("2")),那麼如何使test1函式執行完畢呢?可以使用asyncio自帶的方法迴圈執行協程函式。
await與阻塞
使用async可以定義協程物件,使用await可以針對耗時的操作進行掛起,就像生成器裡的yield一樣,函式讓出控制權。協程遇到await,事件迴圈將會掛起該協程,執行別的協程,直到其他的協程也掛起或者執行完畢,再進行下一個協程的執行,協程的目的也是讓一些耗時的操作非同步化。
注意點:await後面跟的必須是一個Awaitable物件,或者實現了相應協議的物件,檢視Awaitable抽象類的程式碼,表明了只要一個類實現了__await__方法,那麼通過它構造出來的例項就是一個Awaitable,並且Coroutine類也繼承了Awaitable。
自動迴圈執行協程函式
通過前面介紹我們知道執行協程函式需要使用send方法,但一旦協程函式執行過程中切換到其他函式了,那麼這個函式就不在被繼續執行了,並且使用sned方法不是很高效。那麼如何在執行整個程式過程中,自動得執行所有的協程函式呢,就如同多執行緒、多程式那樣,隱式得執行而不是顯示的通過send方法去執行函式。
事件迴圈方法
前面提到的問題就需要用到事件迴圈方法去解決,即asyncio.get_event_loop方法,修改以上程式碼如下:
import asyncio
async def test1():
print("1")
await test2()
print("2")
async def test2():
print("3")
print("4")
loop = asyncio.get_event_loop()
loop.run_until_complete(test1())
複製程式碼
執行以上程式碼得到以下結果:
1
3
4
2
複製程式碼
說明:asyncio.get_event_loop方法可以建立一個事件迴圈,然後使用run_until_complete將協程註冊到事件迴圈,並啟動事件迴圈。
task任務
由於協程物件不能直接執行,在註冊事件迴圈的時候,其實是run_until_complete方法將協程包裝成為了一個任務(task)物件。所謂task物件是Future類的子類,儲存了協程執行後的狀態,用於未來獲取協程的結果。我們也可以手動將協程物件定義成task,修改以上程式碼如下:
import asyncio
async def test1():
print("1")
await test2()
print("2")
async def test2():
print("3")
print("4")
loop = asyncio.get_event_loop()
task = loop.create_task(test1())
loop.run_until_complete(task)
複製程式碼
說明:前面說到task物件儲存了協程執行的狀態,並且可以獲取協程函式執行的返回值,那麼具體該如何獲取呢?這裡可以分兩種方式,一種需要繫結回撥函式,另外一種則直接在執行完task任務後輸出。值得一提的是,如果使用send方法執行函式,則返回值可以通過捕捉StopIteration異常,利用StopIteration.value獲取。
直接輸出task結果
當協程函式執行結束後,我們需要得到其返回值,第一種方式就是等到task狀態為finish時,呼叫task的result方法獲取返回值。
import asyncio
async def test1():
print("1")
await test2()
print("2")
return "stop"
async def test2():
print("3")
print("4")
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(test1())
loop.run_until_complete(task)
print(task.result())
複製程式碼
執行以上程式碼得到以下結果:
1
3
4
2
stop
複製程式碼
回撥函式
獲取返回值的第二種方法是可以通過繫結回撥函式,在task執行完畢的時候可以獲取執行的結果,回撥的最後一個引數是future物件,通過該物件可以獲取協程返回值。
import asyncio
async def test1():
print("1")
await test2()
print("2")
return "stop"
async def test2():
print("3")
print("4")
def callback(future):
print('Callback:',future.result()) # 通過future物件的result方法可以獲取協程函式的返回值
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(test1()) # 建立task,test1()是一個協程物件
task.add_done_callback(callback) # 繫結回撥函式
loop.run_until_complete(task)
複製程式碼
執行以上程式碼得到以下結果:
1
3
4
2
Callback: stop
複製程式碼
如果回撥函式需要接受多個引數,可以通過偏函式匯入,修改程式碼如下:
import asyncio
import functools
async def test1():
print("1")
await test2()
print("2")
return "stop"
async def test2():
print("3")
print("4")
def callback(param1,param2,future):
print(param1,param2)
print('Callback:',future.result())
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(test1())
task.add_done_callback(functools.partial(callback,"param1","param2"))
loop.run_until_complete(task)
複製程式碼
說明:回撥函式中的future物件就是建立的task物件。
future物件
future物件有幾個狀態:Pending、Running、Done、Cancelled。建立future的時候,task為pending,事件迴圈呼叫執行的時候當然就是running,呼叫完畢自然就是done,如果需要停止事件迴圈,就需要先把task取消,可以使用asyncio.Task獲取事件迴圈的task。
協程停止
前面介紹了使用事件迴圈執行協程函式,那麼怎麼停止執行呢?在停止執行協程前,需要先取消task,然後再停止loop事件迴圈。
import asyncio
async def test1():
print("1")
await asyncio.sleep(3)
print("2")
return "stop"
tasks = [
asyncio.ensure_future(test1()),
asyncio.ensure_future(test1()),
asyncio.ensure_future(test1()),
]
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
for task in asyncio.Task.all_tasks():
task.cancel()
loop.stop()
loop.run_forever()
finally:
loop.close()
複製程式碼
執行以上程式碼,按ctrl+c可以結束執行。
本文中用到的一些概念及方法
- event_loop事件迴圈:程式開啟一個無限的迴圈,當把一些函式註冊到事件迴圈上時,滿足事件發生條件即呼叫相應的函式。
- coroutine協程物件:指一個使用async關鍵字定義的函式,它的呼叫不會立即執行函式,而是會返回一個協程物件,協程物件需要註冊到事件迴圈,由事件迴圈呼叫。
- task任務:一個協程物件就是一個原生可以掛起的函式,任務則是對協程進一步封裝,其中包含任務的各種狀態。
- future:代表將來執行或沒有執行的任務的結果,它和task上沒有本質的區別
- async/await關鍵字:python3.5用於定義協程的關鍵字,async定義一個協程,await用於掛起阻塞的非同步呼叫介面。
併發與並行
併發通常指有多個任務需要同時進行,並行則是同一時刻有多個任務執行。用多執行緒、多程式、協程來說,協程實現併發,多執行緒與多程式實現並行。
asyncio協程如何實現併發
asyncio想要實現併發,就需要多個協程來完成任務,每當有任務阻塞的時候就await,然後其他協程繼續工作,這需要建立多個協程的列表,然後將這些協程註冊到事件迴圈中。這裡指的多個協程,可以是多個協程函式,也可以是一個協程函式的多個協程物件。
import asyncio
async def test1():
print("1")
await asyncio.sleep(1)
print("2")
return "stop"
a = test1()
b = test1()
c = test1()
tasks = [
asyncio.ensure_future(a),
asyncio.ensure_future(b),
asyncio.ensure_future(c),
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks)) # 注意asyncio.wait方法
for task in tasks:
print("task result is ",task.result())
複製程式碼
執行以上程式碼得到以下結果:
1
1
1
2
2
2
task result is stop
task result is stop
task result is stop
複製程式碼
說明:程式碼先是定義了三個協程物件,然後通過asyncio.ensure_future方法建立了三個task,並且將所有的task加入到了task列表,最終使用loop.run_until_complete將task列表新增到事件迴圈中。
協程爬蟲
前面介紹瞭如何使用async與await建立協程函式,使用asyncio.get_event_loop建立事件迴圈並執行協程函式。例子很好地展示了協程併發的高效,但在實際應用場景中該如何開發協程程式?比如說非同步爬蟲。我嘗試用requests模組、urllib模組寫非同步爬蟲,但實際操作發現並不支援asyncio非同步,因此可以使用aiohttp模組編寫非同步爬蟲。
aiohttp實現
import asyncio
import aiohttp
async def run(url):
print("start spider ",url)
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
print(resp.url)
url_list = ["https://thief.one","https://home.nmask.cn","https://movie.nmask.cn","https://tool.nmask.cn"]
tasks = [asyncio.ensure_future(run(url)) for url in url_list]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
複製程式碼
執行以上程式碼得到以下結果:
start spider https://thief.one
start spider https://home.nmask.cn
start spider https://movie.nmask.cn
start spider https://tool.nmask.cn
https://movie.nmask.cn
https://home.nmask.cn
https://tool.nmask.cn
https://thief.one
複製程式碼
說明:aiohttp基於asyncio實現,既可以用來寫webserver,也可以當爬蟲使用。
requests實現
由於requests模組阻塞了客戶程式碼與asycio事件迴圈的唯一執行緒,因此在執行呼叫時,整個應用程式都會凍結,但如果一定要用requests模組,可以使用事件迴圈物件的run_in_executor方法,通過run_in_executor方法來新建一個執行緒來執行耗時函式,因此可以這樣修改程式碼實現:
import asyncio
import requests
async def run(url):
print("start ",url)
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, requests.get, url)
print(response.url)
url_list = ["https://thief.one","https://home.nmask.cn","https://movie.nmask.cn","https://tool.nmask.cn"]
tasks = [asyncio.ensure_future(run(url)) for url in url_list]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
複製程式碼
如果要給requests帶上引數,可以使用functools:
import asyncio
import requests
import functools
async def run(url):
print("start ",url)
loop = asyncio.get_event_loop()
try:
response = await loop.run_in_executor(None,functools.partial(requests.get,url=url,params="",timeout=1))
except Exception as e:
print(e)
else:
print(response.url)
url_list = ["https://thief.one","https://home.nmask.cn","https://movie.nmask.cn","https://tool.nmask.cn"]
tasks = [asyncio.ensure_future(run(url)) for url in url_list]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
複製程式碼
asyncio中使用阻塞函式
如同前面介紹如何在asyncio中使用requests模組一樣,如果想在asyncio中使用其他阻塞函式,該怎麼實現呢?雖然目前有非同步函式支援asyncio,但實際問題是大部分IO模組還不支援asyncio。
阻塞函式在asyncio中使用的問題
阻塞函式(例如io讀寫,requests網路請求)阻塞了客戶程式碼與asycio事件迴圈的唯一執行緒,因此在執行呼叫時,整個應用程式都會凍結。
解決方案
這個問題的解決方法是使用事件迴圈物件的run_in_executor方法。asyncio的事件迴圈在背後維護著一個ThreadPoolExecutor物件,我們可以呼叫run_in_executor方法,把可呼叫物件發給它執行,即可以通過run_in_executor方法來新建一個執行緒來執行耗時函式。
run_in_executor方法
AbstractEventLoop.run_in_executor(executor, func, *args)
複製程式碼
- executor 引數應該是一個 Executor 例項。如果為 None,則使用預設 executor。
- func 就是要執行的函式
- args 就是傳遞給 func 的引數
實際例子(使用time.sleep()):
import asyncio
import time
async def run(url):
print("start ",url)
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(None,time.sleep,1)
except Exception as e:
print(e)
print("stop ",url)
url_list = ["https://thief.one","https://home.nmask.cn","https://movie.nmask.cn","https://tool.nmask.cn"]
tasks = [asyncio.ensure_future(run(url)) for url in url_list]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
複製程式碼
執行以上程式碼得到以下函式:
start https://thief.one
start https://home.nmask.cn
start https://movie.nmask.cn
start https://tool.nmask.cn
stop https://thief.one
stop https://movie.nmask.cn
stop https://home.nmask.cn
stop https://tool.nmask.cn
複製程式碼
說明:有了run_in_executor方法,我們就可以使用之前熟悉的模組建立協程併發了,而不需要使用特定的模組進行IO非同步開發。
參考
https://www.oschina.net/translate/playing-around-with-await-async-in-python-3-5 https://www.jianshu.com/p/b5e347b3a17c https://zhuanlan.zhihu.com/p/27258289 https://juejin.im/entry/5aabb949f265da23a04951df
本文來自個人部落格:Python3.5協程學習研究 | nMask'Blog,轉載請說明出處!