Python-asyncio的使用-1

asyncio模塊提供了使用協程構建併發應用的工具。它使用一種單線程單進程的方式實現併發,應用的各個部分彼此合作,可以顯示的切換任務,一般會在程序阻塞I/O操作的時候發生上下文切換如等待讀寫文件,或者請求網絡。同時asyncio也支持調度代碼在將來的某個特定事件運行,從而支持一個協程等待另一個協程完成,以處理系統信號和識別其他一些事件。

對於其他的併發模型大多數採用的都是線性的方式編寫。並且依賴於語言運行時系統或操作系統底層的底層線程或進程來適當的改變上下文,而基於asyncio的應用要求應用代碼顯示的處理上下文切換。asyncio提供的框架以事件循環爲中心,程序開啓一個無限的循環,程序會把一些函數註冊到事件循環當中,當滿足事件發生的時候,調用相應的協程函數。

異步方法

使用asyncio也就意味着你需要一直寫異步方法。一個標準方法是這樣的:

def regular_double(x):
    return 2 * x

而一個異步方法:

async def async_double(x):
    return 2 * x

從外觀上來看,異步方法和標準方法沒什麼區別,只是前面多加了一個asyncasyncasynchronous的簡寫,爲了區別於異步函數,我們稱標準函數爲同步函數。要調用異步函數需要在前面增加一個await關鍵字,但是不能在同步函數中使用await,否則會報錯,需在異步函數中使用!

錯誤寫法:

def print_result():
    print(await async_double(3))

正確寫法:

async print_result():
    print(await async_double(3))

協程

啓動一個協程

一般異步方法被稱之爲協程(Coroutine)。asyncio事件循環可以通過多種不同的方法啓動一個協程。如下:

async def print_message():
    print("這是一個協程")
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        print("開始運行協程")
        coro = print_message()
        print("進入事件循環")
        loop.run_until_complete(coro)
    finally:
        print("關閉事件循環")
        loop.close()

輸出結果如下:

開始運行協程
進入事件循環
這是一個協程
關閉事件循環

這是一個協程的簡單例子:第一步首先獲得一個事件循環的應用就是定義的對象loop。可以使用默認的事件循環,也可以實例化一個特定的循環類(比如uvloop),這裏使用了默認的循環loop.run_until_complete(coro)方法用這個協程啓動這個循環,協程返回時這個方法將停止循環。run_until_complete的參數是一個fetrue對象,當傳入一個協程,其內部會自動封裝成一個task,其中task是fetrue的子類

從協程中返回值

我們將上面的代碼進行修改,如下:

async def print_message():
    print("這是一個協程")
    return "協程返回值"
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        print("開始運行協程")
        coro = print_message()
        print("進入事件循環")
        result = loop.run_until_complete(coro)
        print(f"result: {result}")
    finally:
        print("關閉事件循環")
        loop.close()

輸出結果如下:

開始運行協程
進入事件循環
這是一個協程
result: 協程返回值
關閉事件循環

run_until_complete可以獲取協程的返回值,如果沒有給定返回值,就像普通函數一樣,默認返回None

協程調用協程

一個協程啓動另外一個協程,從而可以任務根據工作內容封裝到不同的協程當中,我們可以使用await關鍵字,鏈式的調度協程,來形成一個協程任務流。如下:

async def main():
    print("這是一個主協程")
    print("等待coroutine_1執行")
    result_1 = await coroutine_1()
    print("等待coroutine_2執行")
    result_2 = await coroutine_2(result_1)
    return result_1, result_2
    
async def coroutine_1():
    print("這是coroutine_1協程")
    return "coroutine_1"
    
async def coroutine_2(args):
    print("這是coroutine_2協程")
    return f"coroutine_1 + {args}"
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        result = loop.run_until_complete(main())
        print(f"result: {result}")
    finally:
        print("關閉事件循環")
        loop.close()

輸出結果如下:

這是一個主協程
等待coroutine_1執行
這是coroutine_1協程
等待coroutine_2執行
這是coroutine_2協程
result: ('coroutine_1', 'coroutine_1 + coroutine_1')
關閉事件循環

協程中調用普通函數

在協程中可以通過一些方法去調用普通的函數。可以使用的關鍵字有call_sooncall_latercall_at

call_soon

call_soon(callback, *args, context=None)

在下一個迭代的事件循環中立刻調用回調函數,大部分的回調函數都支持位置參數,而不支持“關鍵字參數”,如果想要使用關鍵字參數,則推薦使用functools.partial()方法來進行包裝。請看下面例子:

import asyncio
import functools


def call_back(*args, **kwargs):
    print(f"回調函數的參數, args: {args}, kwargs: {kwargs}")
    
async def main(loop):
    print("註冊callback")
    loop.call_soon(call_back, 1)
    wrapper = functools.partial(call_back, name="laozhang")
    loop.call_soon(wrapper, 1)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        print("關閉事件循環")
        loop.close()

輸出結果如下:

註冊callback
回調函數的參數, args: (1,), kwargs: {}
回調函數的參數, args: (1,), kwargs: {'name': 'laozhang'}
關閉事件循環

有時,我們不想立即調用一個函數,此時我們就可以調用call_later延時去調用一個函數了

call_soon_threadsafe

call_soon_threadsafe(callback, *args)

類似call_soon,但是線程安全

call_later

call_later(delay, callback, *args, context=None)

首先簡單說一下它的含義,就是事件循環在delay多長時間之後才執行callback函數,配合上面的call_soon讓我們看一個小例子:

import time
import asyncio

def call_back(*args, **kwargs):
    t = time.time()
    print(f"args: {args}, time: {t}")
    
async def main(loop):
    print("註冊call_back", time.time())
    loop.call_later(1, call_back, "process-1")
    loop.call_later(2, call_back, "process-2")
    loop.call_soon(call_back, "process-3")
    await asyncio.sleep(5)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

輸出結果如下:

註冊call_back 1584685555.1941936
args: ('process-3',), time: 1584685555.1951926
args: ('process-1',), time: 1584685556.1960201
args: ('process-2',), time: 1584685557.196542

從輸出結果可以看出,call_soon是立即執行的,兩個call_later是併發執行的!如果沒有最後的await asyncio.sleep(5),那麼兩句call_later將不會執行,只會執行call_soon!

call_soon會在call_later之前執行,和它的位置在哪無關,call_later的第一個參數越小,越先執行!

call_at

call_at(when, call_back, context=None)

call_at第一個參數的含義代表的是一個單調時間,它和我們平時說的系統時間有點差異,這裏的時間指的是時間循環內部的時間,可以通過loop.time()獲取,然後可以在此基礎上進行操作。後面的參數和前面的兩個方法一樣,實際上call_later內部就是調用的call_at

import asyncio

def call_back(num, loop):
    print(f"call_back函數, num: {num}, time: {loop.time()}")
    
async def main(loop):
    now = loop.time()
    print("註冊call_back函數", now)
    loop.call_at(now + 1, call_back, 1, loop)
    loop.call_at(now + 2, call_back, 2, loop)
    loop.call_soon(call_back, 3, loop)
    await asyncio.sleep(5)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

輸出結果如下:

註冊call_back函數 242044.906
call_back函數, num: 3, time: 242044.906
call_back函數, num: 1, time: 242045.906
call_back函數, num: 2, time: 242046.906

Future

獲取Future的結果

future表示還沒有完成的工作結果,事件循環可以通過監視一個future對象的狀態來指示它已經完成。future對象有幾個狀態:

  • pending
  • running
  • done
  • canceled

創建Future的時候,taskpending,事件循環調用執行的時候自然就是running,調用完畢自然就是done。如果要停止事件循環,就需要先把task取消,狀態爲cancel

import asyncio

def call_back(future, result):
    print(f"進入call_back函數, future: {future}, result: {result}")
    future.set_result(result)
    print(f"此時future的結果爲: {future}")

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        future = asyncio.Future()
        loop.call_soon(call_back, future, "result is finished")
        result = loop.run_until_complete(future)
        print(f"run_until_complete, result: {result}")
    finally:
        loop.close()

輸出結果如下:

進入call_back函數, future: <Future pending cb=[_run_until_complete_cb() at ...]>, result: result is finished
此時future的結果爲: <Future finished result='result is finished'>
run_until_complete, result: result is finished

通過輸出結果可以發現,調用set_result之後,對象的狀態會由pending變爲finishedfuture的實例會保留提供給方法的結果,可以在後續使用。

future對象使用await

future可以像協程一樣使用await關鍵字獲取結果

import asyncio

def call_back(future, result):
    print(f"進入call_back函數, future: {future}, result: {result}")
    future.set_result(result)

async def main(loop):
    print(f"進入main異步函數")
    future = asyncio.Future()
    loop.call_soon(call_back, future, "result is finished")
    print(await future)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

輸出結果如下:

進入main異步函數
進入call_back函數, future: <Future pending cb=[<TaskWakeupMethWrapper object at 0x0000028A3A932468>()]>, result: result is finished
result is finished

future回調

future在完成的時候可以執行一些回調函數,回調函數按照註冊時的順序進行調用

import asyncio
import functools

def call_back(future, n):
    print(f"進入call_back函數, future: {future}, n: {n}")
    
async def register_callbacks(future):
    print("註冊call_back")
    future.add_done_callback(functools.partial(call_back, n=1))
    future.add_done_callback(functools.partial(call_back, n=2))
    
async def main(future):
    await register_callbacks(future)
    print("設置future的結果")
    future.set_result("result is finished")

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        future = asyncio.Future()
        loop.run_until_complete(main(future))
    finally:
        loop.close()

輸出結果如下:

註冊call_back
設置future的結果
進入call_back函數, future: <Future finished result='result is finished'>, n: 1
進入call_back函數, future: <Future finished result='result is finished'>, n: 2

通過add_done_callback方法給future添加回調函數,當future執行完成的時候,就會調用回調函數。並通過future獲取協程執行的結果

併發的執行任務

任務(Task)是與事件循環交互的主要途徑條件之一。任務可以包裝成爲協程,可以跟蹤協程何時完成,任務是Future的子類,所以使用方法和Future一樣。協程可以等待任務,每個任務都有一個結果,在它完成之後可以獲取這個結果。因爲協程是無狀態的,我們通過使用create_task方法,可以將協程包裝成有狀態的任務。還可以在任務運行的過程中取消任務。

import asyncio

async def child():
    print("進入child協程")
    return "result is ok!"
    
async def main(loop):
    print("進入main協程")
    task = loop.create_task(child())
    print("cancel task")
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("取消任務拋出CancelledError")
    else:
        print("任務的結果: ", task.result())
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

輸出結果如下:

進入main協程
cancel task
取消任務拋出CancelledError

如果我們把上面的task.cancel()註釋掉,那麼我們就可以獲得正常請看下的結果了,如下:

進入main協程
cancel task
進入child協程
任務的結果:  result is ok!

同樣,我們可以使用await關鍵字來獲取任務的結果!

res = await task

除了create_task,我們還可以使用asyncio.ensure_future(coroutine)創建一個task

組合協程

一系列的協程可以通過await鏈式的調用,但是有的時候我們需要在一個協程裏等待多個協程,比如我們在一個協程裏等待1000多個異步網絡請求,對於訪問次序沒有要求的時候,就可以使用另外的關鍵字awaitgather來解決了。await可以暫停一個協程,直到後臺操作完成。

等待多個協程

import asyncio

async def child(n):
    print("進入child協程")
    try:
        await asyncio.sleep(n * 0.1)
        return n
    except asyncio.CancelledError:
        print("數字{n}被取消".format(n=n))
        raise
         
async def main():
    tasks = [child(i) for i in range(10)]
    complete, pending = await asyncio.wait(tasks, timeout=0.5)
    print("complete", complete)
    print("pending", pending)
    for task in complete:
        print("當前數字爲: {result}".format(result=task.result()))
        
    for task in pending:
        task.cancel()
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
complete {<Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=4>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=0>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=3>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=1>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=5>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=2>}
pending {<Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30798>()]>>, <Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30738>()]>>, <Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30858>()]>>, <Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30918>()]>>}
當前數字爲: 4
當前數字爲: 0
當前數字爲: 3
當前數字爲: 1
當前數字爲: 5
當前數字爲: 2
數字7被取消
數字6被取消
數字8被取消
數字9被取消

可以發現我們的結果並沒有按照數字的順序顯示,在內部await()使用一個set()保存它創建的Task實例。因爲set是無序的,所以這也就是我們的任務沒有按順序執行的原因。await的返回值是一個元組,包括兩個集合,分別表示已完成任務和未完成任務。wait函數中timeout參數表示超時值,達到這個超時時間之後,未完成的任務狀態變爲pending,當程序退出時還有任務沒有完成時,就會看到如下的錯誤提示:

Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0738>()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0798>()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0858>()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0918>()]>>

我們可以通過迭代調用cancel方法來取消任務,也就是如下這段代碼:

for task in pending:
    task.cancel()

gather的作用和wait類似,但不同的是:

  1. gather任務無法取消
  2. 返回值是一個結果列表
  3. 可以按照傳入的參數的順序,順序輸出

我們將上面的代碼進行修改一些,使用gather的方式:

import asyncio

async def child(n):
    print("進入child協程")
    try:
        await asyncio.sleep(n * 0.1)
        return n
    except asyncio.CancelledError:
        print("數字{n}被取消".format(n=n))
        raise
        
async def main():
    tasks = [child(i) for i in range(10)]
    complete = await asyncio.gather(*tasks)
    print(complete)
    for result in complete:
        print("當前數字爲: {result}".format(result=result))
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
進入child協程
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
當前數字爲: 0
當前數字爲: 1
當前數字爲: 2
當前數字爲: 3
當前數字爲: 4
當前數字爲: 5
當前數字爲: 6
當前數字爲: 7
當前數字爲: 8
當前數字爲: 9

gather通常被用來階段性的一個操作,做完第一步才能做第二步,比如下面操作:

import asyncio 
import time

async def child_1(timeout, now):
    await asyncio.sleep(timeout)
    print("第一階段完成, 需時: {}".format(time.time() - now))
    return timeout

async def child_2(timeout, now):
    await asyncio.sleep(timeout)
    print("第二階段完成, 需時:{}".format(time.time() - now))    
    return timeout

async def main():
    now = time.time()
    result_list = await asyncio.gather(child_1(5, now), child_2(2, now))
    for result in result_list:
        print("result: {}".format(result))

    print("總需時: {}".format(time.time() - now))

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

第二階段完成, 需時:2.0005438327789307
第一階段完成, 需時: 5.0014026165008545
result: 5
result: 2
總需時: 5.0014026165008545

通過上面的輸出結果可以得到以下結論:

  1. child_1與child_2是併發運行的
  2. gather會等那個最耗時的任務完成之後才返回結果,耗時總時間取決於其中任務最長時間的那個

任務完成時進行處理

as_completed方法返回一個生成器,會管理一個指定的任務列表,並生成它們的結果。每個協程結束運行時一次生成一個結果,與wait一樣,as_completed不能保證順序,不過執行其他動作之前沒有必要等待所有後臺操作完成

import asyncio

async def foo(n):
    print("Waiting: ", n)
    await asyncio.sleep(2)
    return n
    
async def main():
    tasks = [asyncio.ensure_future(foo(i)) for i in range(10)]
    result_list = asyncio.as_completed(tasks)
    print("result_list", result_list)
    for result in result_list:
        print("Task Res: {}, Content: {}".format(result, await result))
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

輸出結果如下:

result_list <generator object as_completed at 0x0000013D1FF36308>
Waiting:  0
Waiting:  1
Waiting:  2
Waiting:  3
Waiting:  4
Waiting:  5
Waiting:  6
Waiting:  7
Waiting:  8
Waiting:  9
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 0
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 2
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 6
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 9
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 8
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 5
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 7
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 4
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 1
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 3
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章