asyncio 非同步程式設計

Yang`發表於2022-05-03

首先了解一下協程,協程的本質就是一條執行緒,多個任務在一條執行緒上來回切換,協程的所有切換都是基於使用者,只有在使用者級別才能感知到的 IO 才會用協程模組來規避,在 python 中主要使用的協程模組是 asyncio,並且基於 async 和 await 關鍵字的協程可以實現非同步程式設計,這也是目前 python 非同步相關的主流技術。

1.事件迴圈

事件迴圈它其實是非同步程式設計中的一個非常重要的環節,可以把它當成一個死迴圈,它會去檢查並執行一些程式碼。

示例:虛擬碼

任務列表 = [ 任務1, 任務2, 任務3,... ]

while True:
    可執行的任務列表,已完成的任務列表 = 去任務列表中檢查所有的任務,將'可執行'和'已完成'的任務返回
    
    for 就緒任務 in 已準備就緒的任務列表:
        執行已就緒的任務
    
    for 已完成的任務 in 已完成的任務列表:
        在任務列表中移除 已完成的任務
    
    如果 任務列表 中的任務都已完成,則終止迴圈

通過上述虛擬碼就會發現這個事件迴圈就是可以理解成一個死迴圈在檢查一個列表裡的任務,如果列表裡面的任務是可執行的,那就去執行這個任務,如果是不可執行(指的是遇到 IO 操作)的,那麼在檢查的時候就根本檢查不到,相當於把 這個任務忽略掉,認為它不需要被執行,讓它一直在等待著 IO 請求,當 IO 完成之後在去執行這個任務。

獲取和建立事件迴圈

import asyncio

# 生成和獲取一個事件迴圈
loop = asyncio.get_event_loop()

# 給事件迴圈新增任務,讓事件迴圈去檢測這個任務的狀態是否可執行
loop.run_until_complete(任務)

2.async

async 是一個關鍵字,用於定義一個協程函式。

協程函式:定義函式的時候使用 async def 函式名

協程物件:執行 協程函式() 得到的協程物件。

# 定義一個協程函式
async def func():
    pass

# 呼叫協程函式,返回一個協程物件
result = func()

呼叫協程函式的時候,函式內部的程式碼不會執行,只會返回一個協程物件。

如果想要執行協程函式內部程式碼,必須要將協程物件交給事件迴圈來處理。

import asyncio

async def func():
    print('這是一個協程函式!')
    
    
result = func

# 方式一:
loop = asyncio.get_event_loop()   # 生成一個事件迴圈
loop.run_until_complete( result ) # 將協程物件新增到事件迴圈執行


# 方式二:python 3.7 之後使用,本質上還是和上面一樣,但是比較簡單
asyncio.run( result )

3.await

await 也是一個關鍵字,它主要是在當前任務1遇到 IO 操作的時候切到其他沒有 IO 操作的任務2去執行,讓事件迴圈可以去執行其他任務,當任務1的 IO 操作執行完後再切換回來執行 await 之後的內容。

await 的後面只能加可等待的物件(協程物件、Task物件 ....)

示例:

import asyncio


async def others():
    print('others -----> start')
    await asyncio.sleep(2)
    print('others -----> end')
    return '返回值'


async def fun():
    print('執行協程函式內部程式碼!')
    
    # 遇到IO操作掛起當前協程(任務),等IO操作完成之後再繼續往下執行。當前協程掛起時,事件迴圈可以去執行其他協程(任務)。
    response = await others()	# 它會等有返回值了才會繼續往下執行
    print('IO請求結束,結果為:', response)

asyncio.run(fun())


# 輸出:
執行協程函式內部程式碼!
others -----> start
others -----> end
IO請求結束,結果為: 返回值

從上面這個示例可以看出來 await 就是等待對應後面的值得到結果之後,在向下繼續執行!

由於在這個示例中事件迴圈列表中只有一個任務,所以在 IO 等待時無法演示切換到其他任務的執行效果,在程式中如果想要建立多個任務物件,需要使用 Task 物件來實現。

4.Task物件

Task 用於併發排程協程,在事件迴圈中新增多個任務。

本質上是將協程物件封裝成 Task 物件,並將該協程加入事件迴圈,同時追蹤協程的狀態。

示例1:通過asyncio.create_task(協程物件)新增任務。

import asyncio


async def func(i):
    print(i, '--->start')
    await asyncio.sleep(1)
    print(i, '--->end')
    return f'返回值{i}'


async def main():
    print('main start')

    # 建立 Task 物件並新增到事件迴圈中
    task1 = asyncio.create_task(func(1))
    task2 = asyncio.create_task(func(2))

    print('main end')
	
    # 此處await會自動切換執行其他任務。例如:task1,task2
    ret1 = await task1
    ret2 = await task2
    print(ret1, ret2)

asyncio.run(main())

# 輸出:
main start
main end
1 --->start
2 --->start
1 --->end
2 --->end
返回值1 返回值2

示例2:通過asyncio.wait(協程物件列表)新增任務,在它的原始碼中會通過 ensure_future 把每個協程封裝成 Task 物件。

import asyncio


async def func(i):
    print(i, '--->start')
    await asyncio.sleep(1)  # 當遇到IO操作掛起當前協程並切換其他協程
    print(i, '--->end')
    return f'返回值{i}'


task_list = [func(1), func(2)]

# 如果設定了 timeout 值,則意味著此處最多等待的秒,完成的協程返回值寫入done中,未完成的寫入pending
done, pending = asyncio.run(asyncio.wait(task_list, timeout=None))


# 輸出:
1 --->start
2 --->start
1 --->end
2 --->end

相關文章