一. asyncio基本操作
1.1 任務狀態
上一節我們提到asyncio的任務使用協程對象作爲參數創建。並且任務含有多種狀態。下面我們使用一個簡單的例子來說明任務的各種狀態。
import time
import asyncio
@asyncio.coroutine
def do_some_work():
print('Coroutine start.')
time.sleep(3)
print('Coroutine finished.')
def main():
start = time.time()
loop = asyncio.get_event_loop()
coroutine = do_some_work()
task = loop.create_task(coroutine) # 創建任務
print('task is instance of asyncio.Task?', 'yes' if isinstance(task, asyncio.Task) else 'No')
print(f'task state {task._state}')
loop.run_until_complete(task)
print(f'task state {task._state}')
end = time.time()
print(f'運行耗時: {end-start:.2f}')
if __name__ == '__main__':
main()
運行結果:事件循環的 create_task 方法可以創建任務,另外 asyncio.ensure_future 方法也可以創建任務,參數須爲協程對象。task 是 asyncio.Task 類的實例,創建 task 可以方便預激協程以及處理協程運行中遇到的異常。task 對象的 _state 屬性保存當前任務的運行狀態,任務的運行狀態有 PENDING 和 FINISHED 兩種。
1.2 async / await關鍵字
Python3.5新增的async和await關鍵字可以用來定義協程函數。這兩個關鍵字是一個組合,其作用等同於@asyncio.coroutine裝飾器和yield from語句。以便將協程函數和生成器函數在語法上做出明顯的區分。
1.3 綁定回調
假設協程包含一個IO操作(這幾乎是肯定的),等它處理完數據後,我們希望得到通知,以便下一步數據處理。這一需求可以通過向future對象添加回調實現。那什麼是future對象呢?task對象就是future對象,因爲asyncio.Task是asyncio.Future的子類。因此task對象也可以添加回調函數。回調函數的最後一個參數是future或task對象,通過該對象可以獲取協程返回值。如果回調需要多個參數,可以使用functools.partial偏導函數傳入。
import asyncio
import time
from functools import partial
async def coro_work():
print('coro_work -> Coroutine start.')
time.sleep(3)
print('coro-work -> Coroutine finished.')
def callback(name, task):
print(f'callback -> {task._state}')
print(f'callback -> {name}')
def main():
start = time.time()
loop = asyncio.get_event_loop()
coroutine = coro_work()
task = loop.create_task(coroutine)
task.add_done_callback(partial(callback, 'Coroutine, Bye Bye~'))
loop.run_until_complete(task)
end = time.time()
print(f'運行耗時:{end - start:.2f}')
if __name__ == '__main__':
main()
運行結果:使用 async 關鍵字替代 asyncio.coroutine 裝飾器創建協程函數。callback爲回調函數,協程終止後需要運行的代碼寫入回調函數,回調函數的參數有要求,最後一個位置參數須爲 task 對象。task 對象的 add_done_callback 方法可以添加回調函數,注意參數必須是回調函數,這個方法不能傳入回調函數的參數,這一點需要通過 functools 模塊的 partial 方法解決,將回調函數和其參數 name 作爲 partial 方法的參數,此方法的返回值就是偏函數,偏函數可作爲 task.add_done_callback 方法的參數。
二. 協程處理多任務
開始介紹asyncio模塊到現在,我們還沒有使用協程處理多任務。在實際項目中,往往有多個協程對象,並創建多個任務,同時在一個loop裏運行。爲了把多個協程交給loop,需要藉助asyncio.gather方法。任務的result方法可以獲得對應協程函數的return值。
import asyncio
import time
async def coro_work(name, t):
print(f'[coro_work] Coroutine {name} start.')
await asyncio.sleep(t)
print(f'[coro_work] Coroutine {name} finished.')
return f'Coroutine {name} OK.'
def main():
start = time.time()
loop = asyncio.get_event_loop()
coroutine1 = coro_work('ONE', 3)
coroutine2 = coro_work('TWO', 1)
task1 = loop.create_task(coroutine1)
task2 = asyncio.ensure_future(coroutine2)
gather = asyncio.gather(task1, task2)
loop.run_until_complete(gather)
print(f'[task1 result] {task1.result()}')
print(f'[task2 result] {task2.result()}')
end = time.time()
print(f'運行耗時:{end-start:.4f}')
if __name__ == '__main__':
main()
代碼說明:
await關鍵字等同於python3.4中的yield from語句,後面接協程對象。asyncio.sleep方法的返回值爲協程對象,此處爲阻塞運行。asyncio.sleep與time.sleep是不同的,前者阻塞當前協程,即coro_work函數的運行,而time.sleep會阻塞整個線程,所以此處使用前者,阻塞當前協程,CPU可以在線程內的其它協程運行。
協程函數的return值在協程運行結束後通過調用對應task對象的result方法返回。asyncio.gather方法接收多個task作爲參數,創建任務蒐集器。注意,asyncio.gather方法中參數的順序決定了協程的啓動順序。時間循環的run_until_complete方法也可接收任務蒐集器作爲參數,並阻塞運行,直到全部任務完成。任務結束後,事件循環終止,打印任務的result方法返回值,即協程函數的return值。
運行結果:在事件循環內部,2個協程時交替運行完成的:首先運行task1,打印“[coro_work] Coroutine ONE start.”,task1運行到asyncio.sleep阻塞,讓步CPU的使用權給task2執行,打印“[coro_work] Coroutine TWO start.”,task2運行到asyncio.sleep阻塞,再次讓步CPU的使用權,但此刻事件循環發現所有協程都處於阻塞狀態,只能等待阻塞結束。task2的阻塞時間較短,阻塞1s後結束,打印“[coro_work] Coroutine TWO finished.”;又過了2s,阻塞3s的task1也結束了,打印“[coro_work] Coroutine ONE finished.”。至此,2個任務全部完成,事件循環停止,打印task1和task2的返回值,任務總耗時約3s,如果使用單線程同步模型則至少4s。
注意:
- 多數情況下無需調用task的add_done_callback方法,可以直接把回調函數中的代碼寫入await語句後面,協程是可以暫停和恢復的。
- 多數情況下同樣不需要調用task的result方法獲取協程函數的return值,因爲事件循環的run_until_complete方法的返回值就是協程函數的返回值。
- 事件循環有一個stop方法來停止循環和一個close方法來關閉循環。以上示例均沒有調用loop.close方法,似乎並沒有什麼問題,那調用loop.close是否是必須的呢?簡言之,loop只要不關閉,就可以再次運行run_until_complete()方法,關閉後則不可運行。有人建議調用loop.close,以徹底清理loop對象防止誤用,其實多數情況下並無必要。
- asyncio提供了asyncio.gather和asyncio.wait兩個任務蒐集方法,它們的作用相同,都是將協程任務按順序排定,再將返回值作爲參數加入到事件循環中。二者的主要區別在於,asyncio.wait可以獲取任務的執行狀態(PENDING & FINISHED),當有一些特殊需求,比如某些情況下取消任務,可以使用asyncio.wait蒐集器。
三. 取消任務
在事件循環啓動之後,停止之前,我們可以手動取消任務的執行,但注意只有PENDING狀態的任務才允許取消,FINISHED狀態的任務已經完成,自然無法取消。
import asyncio
async def work(id, t):
print('Working...')
await asyncio.sleep(t)
print(f'Work {id} done.')
def main():
loop = asyncio.get_event_loop()
coroutines = [work(i, i) for i in range(1, 4)]
try:
loop.run_until_complete(asyncio.gather(*coroutines))
except KeyboardInterrupt:
loop.stop() # 取消所有未完成的任務,停止事件循環
finally:
loop.close() # 關閉事件循環
if __name__ == '__main__':
main()
運行結果:程序運行過程中,按Ctrl + C會觸發KeyboardInterrupt異常。捕獲這個異常,將取消所有未完成的任務。
除了使用事件循環的stop方法取消所有未完成的任務,還可以直接調用任務的cancel方法,而asyncio.Task.all_tasks方法可以獲得事件循環中的全部任務。讓我們修改上個實例的main()函數代碼:
import asyncio
async def work(id, t):
print('Working...')
await asyncio.sleep(t)
print(f'Work {id} done.')
def main():
loop = asyncio.get_event_loop()
coroutines = [work(i, i) for i in range(1, 4)]
try:
loop.run_until_complete(asyncio.gather(*coroutines))
except KeyboardInterrupt:
# loop.stop() # 取消所有未完成的任務,停止事件循環
print()
tasks = asyncio.Task.all_tasks()
for task in tasks:
print(f'正在取消任務:{task}')
print(f'任務取消:{task.cancel()}')
finally:
loop.close() # 關閉事件循環
if __name__ == '__main__':
main()
運行結果: 程序運行到work 1 done輸出時,按下 Ctrl + C 會觸發 KeyboardInterrupt 異常。asyncio.Task.all_tasks()可以捕獲事件循環(每個線程只能有一個事件循環)中的所有任務的集合,任務狀態有PENDING和FINISHED兩者。任務的cancel方法可以取消未完成的任務,取消成功返回True,已完成的任務由於取消失敗返回False。