python實現協程(五)

一. 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。

注意:

  1. 多數情況下無需調用task的add_done_callback方法,可以直接把回調函數中的代碼寫入await語句後面,協程是可以暫停和恢復的。
  2. 多數情況下同樣不需要調用task的result方法獲取協程函數的return值,因爲事件循環的run_until_complete方法的返回值就是協程函數的返回值。
  3. 事件循環有一個stop方法來停止循環和一個close方法來關閉循環。以上示例均沒有調用loop.close方法,似乎並沒有什麼問題,那調用loop.close是否是必須的呢?簡言之,loop只要不關閉,就可以再次運行run_until_complete()方法,關閉後則不可運行。有人建議調用loop.close,以徹底清理loop對象防止誤用,其實多數情況下並無必要。
  4. 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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章