python asyncio 異步 I/O - 協程(Coroutine)與運行


前言

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等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM