asyncio
模塊提供了使用協程構建併發應用的工具。它使用一種單線程單進程的方式實現併發,應用的各個部分彼此合作,可以顯示的切換任務,一般會在程序阻塞I/O操作的時候發生上下文切換如等待讀寫文件,或者請求網絡。同時asyncio
也支持調度代碼在將來的某個特定事件運行,從而支持一個協程等待另一個協程完成,以處理系統信號和識別其他一些事件。
對於其他的併發模型大多數採用的都是線性的方式編寫。並且依賴於語言運行時系統或操作系統底層的底層線程或進程來適當的改變上下文,而基於asyncio
的應用要求應用代碼顯示的處理上下文切換。asyncio
提供的框架以事件循環爲中心,程序開啓一個無限的循環,程序會把一些函數註冊到事件循環當中,當滿足事件發生的時候,調用相應的協程函數。
異步方法
使用asyncio
也就意味着你需要一直寫異步方法。一個標準方法是這樣的:
def regular_double(x):
return 2 * x
而一個異步方法:
async def async_double(x):
return 2 * x
從外觀上來看,異步方法和標準方法沒什麼區別,只是前面多加了一個async
。async
是asynchronous
的簡寫,爲了區別於異步函數,我們稱標準函數爲同步函數。要調用異步函數需要在前面增加一個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_soon
、call_later
和call_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
的時候,task
爲pending
,事件循環調用執行的時候自然就是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
變爲finished
,future
的實例會保留提供給方法的結果,可以在後續使用。
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多個異步網絡請求,對於訪問次序沒有要求的時候,就可以使用另外的關鍵字await
或gather
來解決了。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
類似,但不同的是:
- gather任務無法取消
- 返回值是一個結果列表
- 可以按照傳入的參數的順序,順序輸出
我們將上面的代碼進行修改一些,使用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
通過上面的輸出結果可以得到以下結論:
- child_1與child_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