前言
Python 在 3.5 版本中引入了關於協程的語法糖 async 和 await, 在 python3.7 版本可以通過 asyncio.run() 運行一個協程。
所以建議大家學習協程的時候使用 python3.7+ 版本,本文示例代碼在 python3.8 上運行的。
協程 coroutines
協程(coroutines)通過 async/await 語法進行聲明,是編寫 asyncio 應用的推薦方式。
例如,以下代碼段(需要 Python 3.7+)
import asyncio
import time
async def fun():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
# 運行
print(fun())
當我們直接使用fun() 執行的時候,運行結果是一個協程對象coroutine object
,並且會出現警告
RuntimeWarning: coroutine 'fun' was never awaited
print(fun())
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
在函數前面加了async
,這就是一個協程了,運行的時候需使用asyncio.run()來執行(需要 Python 3.7+)
import asyncio
import time
async def fun():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
# 運行
asyncio.run(fun())
運行結果
hello start: 1646009849.5220373
------hello end : 1646009852.5258074 ----
協程運行三種機制
要真正運行一個協程,asyncio 提供了三種主要機制:
- asyncio.run() 函數用來運行最高層級的入口點 "fun()" 函數 (參見上面的示例。)
- 等待一個協程。 如:await asyncio.sleep(3)
- asyncio.create_task() 函數用來並發運行作為 asyncio 任務 的多個協程。
通過前面第一個示例,知道了asyncio.run()
來運行一個協程,接着看 await 等待的使用
import asyncio
import time
async def fun_a():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
async def fun_b():
print(f"world start: {time.time()}")
await asyncio.sleep(2)
print(f'------world end : {time.time()} ----')
async def main():
print('start main:')
await fun_a()
await fun_b()
print('-----------end start----------')
asyncio.run(main())
運行結果
start main:
hello start: 1646010206.405429
------hello end : 1646010209.4092102 ----
world start: 1646010209.4092102
------world end : 1646010211.4115622 ----
-----------end start----------
運行的入口是main(), 遇到await 會先去執行 fun_a(),執行完成后再去執行fun_b()。
需注意的是,await 后面不能是普通函數,必須是一個可等待對象(awaitable object),Python 協程屬於 可等待 對象,因此可以在其他協程中被等待。
如果一個對象能夠被用在 await表達式中,那么我們稱這個對象是可等待對象(awaitable object)。很多asyncio API都被設計成了可等待的。
主要有三類可等待對象:
- 協程coroutine
- 任務Task
- 未來對象Future。
在前面這個示例中,fun_a() 和 fun_b()是按順序執行的,這跟我們之前寫的函數執行是一樣的,看起來沒啥差別,接着看如何並發執行2個協程任務
asyncio.create_task() 函數用來並發運行作為 asyncio 任務的多個協程
import asyncio
import time
async def fun_a():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
async def fun_b():
print(f"world start: {time.time()}")
await asyncio.sleep(2)
print(f'------world end : {time.time()} ----')
async def main():
print('start main:')
task1 = asyncio.create_task(fun_a())
task2 = asyncio.create_task(fun_b())
await task1
await task2
print('-----------end start----------')
asyncio.run(main())
運行結果
start main:
hello start: 1646010554.0892649
world start: 1646010554.0892649
------world end : 1646010556.108237 ----
------hello end : 1646010557.08811 ----
-----------end start----------
從運行的結果可以看到,hello start 和 world start 的開啟時間是一樣的,也就是2個任務是並發執行的。
並發任務的誤區
當我們知道協程可以實現並發后,於是小伙伴就想小試一下,去模擬並發下載圖片,或者去並發訪問網站。
先看第一個誤區:
把上一個示例中的 await asyncio.sleep(3)
換成 time.sleep(3)
,假設是完成任務需花費的時間。
import asyncio
import time
async def fun_a():
print(f'hello start: {time.time()}')
time.sleep(3) # 假設是執行請求花費的時間
print(f'------hello end : {time.time()} ----')
async def fun_b():
print(f"world start: {time.time()}")
time.sleep(2) # 假設是執行請求花費的時間
print(f'------world end : {time.time()} ----')
async def main():
print('start main:')
task1 = asyncio.create_task(fun_a())
task2 = asyncio.create_task(fun_b())
await task1
await task2
print('-----------end start----------')
asyncio.run(main())
運行結果
start main:
hello start: 1646010901.340716
------hello end : 1646010904.3481765 ----
world start: 1646010904.3481765
------world end : 1646010906.3518314 ----
-----------end start----------
從運行結果看到,並沒有實現並發的效果。這是因為time.sleep()它是一個同步阻塞的模塊,不是異步庫,達不到並發的效果。
同樣道理,之前很多同學學過的 requests 庫,知道 requests 庫可以發請求,於是套用上面的代碼,也是達不到並發效果. 因為 requests 發送請求是串行的,即阻塞的。發送完一條請求才能發送另一條請求。
如果想實現並發請求,需用到發送 http 請求的異步庫,如:aiohttp,grequests等。