30. 協程

hbutmeng發表於2024-10-04

1.協程的概念

1.1 定義

程序是作業系統內部執行的程式

執行緒是程序內部執行的程式

協程是執行緒內部執行的程式

協程是單執行緒下的併發,又成微執行緒,英文名coroutine

1.2 協程的優點

協程切換的開銷更小

GIL鎖導致同一時刻只能執行一個執行緒,一個執行緒內不會限制協程數,單執行緒就可以實現併發的效果,最大程度利用CPU

1.3 協程的缺點

協程的本質是單執行緒下的多工處理,無法利用多核優勢

如果協程發生阻塞,在沒有使用非同步I/O的情況下,那麼整個執行緒將阻塞,所線上程的其它協程任務都不能執行

2. 協程的操作

2.1 協程函式

加上async關鍵字的函式就是協程函式

async def f1():
    ...

2.2 協程物件

協程函式呼叫後的返回值就是協程物件

async def f1():
    print(111)


res = f1()
print(res)

按一般函式的呼叫方法呼叫協程函式不會報錯,但是會發出警告

按一般函式的呼叫方法呼叫協程函式,函式內部的程式碼不會執行,只是會返回一個協程物件。

2.3 asyncio模組

Python 3.4:asyncio 被正式引入標準庫,是一個實現非同步程式設計的模組。
Python 3.5:引入了 async 和 await 關鍵字,這使得協程的定義和使用更加直觀和簡潔。這些關鍵字取代了早期版本中的 @asyncio.coroutine 裝飾器和 yield from 語法。

2.4 事件迴圈的概念

事件迴圈可以類比while迴圈來理解,在迴圈週期內執行一些任務,特定的條件下結束迴圈。

import asyncio

loop = asyncio.get_event_loop()

2.5 協程函式的兩種呼叫方法,以直譯器3.10版本為例

由協程物件的定義可知,按函式名( )無法呼叫協程函式,需要協程物件和事件迴圈配合才能實現

[1]方法一:

import asyncio

async def work():
    print(666)
    return 'work函式的返回值'

def create_coroutine():
    obj = work()  # 1.呼叫協程函式,生成協程物件
    circle = asyncio.get_event_loop()  # 2.建立一個事件迴圈
    circle.run_until_complete(obj)  # 3.將協程物件當作任務提交到事件迴圈的任務列表中,協程執行完成事件迴圈停止

if __name__ == '__main__':
    create_coroutine()

在 Python 3.10 中,這個警告是因為在沒有活動事件迴圈的主執行緒中呼叫 asyncio.get_event_loop() 時,get_event_loop() 會自動建立一個新的事件迴圈,這種方式被認為是不推薦的。
為了避免這個警告,可以使用 asyncio.run() 來執行協程,這是從 Python 3.7 開始推薦的做法。asyncio.run() 會自動建立和關閉事件迴圈,簡化了非同步程式碼的編寫。

[2]方法二:

import asyncio

async def work():
    print(666)
    return 'work函式的返回值'

def create_coroutine():
    obj = work()  # 1.呼叫協程函式,生成協程物件
    asyncio.run(obj)  # 2.呼叫run函式啟動協程物件

if __name__ == '__main__':
    create_coroutine()

方式二的本質和方式一是一樣的,內部先建立事件迴圈,然後執行run_until_complete

需要注意的是,run函式在直譯器3.7才加入

2.6 await關鍵字

await關鍵字用於等待一個async(非同步)函式的結果,使用await可以讓程式在等待某個操作完成的同時不阻塞整個事件迴圈,從而允許其它任務執行,對於I/O密集型的應用特別有用。

await關鍵字解決了一個協程阻塞而導致整個執行緒阻塞的問題。

注意事項:

1.使用await必須在async(非同步)函式中,不能在非非同步函式中使用。

2.await只能用於非同步函式返回的協程物件。

3.在非同步函式內部,await可以用來等待另一個非同步函式的結果,這會暫停當前協程的執行,直到等待的協程完成,然後繼續執行。

程式碼示例

import asyncio

async def f1():
    print('非同步函式執行開始')
    await asyncio.sleep(1)  # 模擬非同步操作,比如網路請求
    print('非同步函式執行結束')
    return 666

async def f2():
    res = await f1()
    print(res)

asyncio.run(f2())  # 獲取事件迴圈並執行f2()協程

分析:

f1是一個非同步函式,使用了await來等待asyncio.sleep(1)。這實際上並不會讓程式等待1秒,而是允許事件迴圈在這1秒內執行其它任務。

f2也是一個非同步函式,等待f1的執行結果。

ascyncio.run(f2())啟動事件迴圈並執行f2協程。

2.7 Task物件

[1]概念

前面的協程程式碼都只建立了一個任務,即事件迴圈的列表中只有一個任務物件;如需在程式中建立多個任務物件,需要使用Task。

Task用於併發排程協程,透過asyncio.create(協程物件)的方式建立Task物件,可以讓協程加入事件迴圈中等待被排程執行。

注意事項:

asyncio.create_task( )函式在python3.7中被加入,在之前的版本可以改用底層級的asyncio.ensure_future( )函式。

[2]建立多工方式一

逐個建立任務

import asyncio

# 定義協程功能函式
async def work():
    print('協程執行開始')
    await asyncio.sleep(1)  # 模擬非同步操作,比如網路請求
    print('協程執行結束')
    return 666

# 定義產生協程函式
async def create_coroutine():
    print('產生協程的函式執行開始')
    task1 = asyncio.create_task(work())  # 將work()得到的協程物件封裝到Task物件中,並立即新增到事件迴圈的任務列表中,等待事件迴圈
    task2 = asyncio.create_task(work())  # 將work()得到的協程物件封裝到Task物件中,並立即新增到事件迴圈的任務列表中,等待事件迴圈

    response1 = await task1  # 引用了await之後,task1遇到sleep不會阻塞整個執行緒
    response2 = await task2

    print(response1, response2)
    print('產生協程的函式執行結束')

if __name__ == '__main__':
    asyncio.run(create_coroutine())

[3]建立多工方式二

使用列表生成式生成任務

import asyncio

# 定義協程功能函式
async def work(num):
    print('協程執行開始')
    await asyncio.sleep(1)  # 模擬非同步操作,比如網路請求
    print('協程執行結束')
    return num * num

# 定義產生協程函式
async def create_coroutine():
    print('產生協程的函式執行開始')
    # 將work()得到的協程物件封裝到Task物件中,並立即新增到事件迴圈的任務列表中,等待事件迴圈
    task_list = [asyncio.create_task(work(i)) for i in range(1, 4)]

    # 引用了await之後,task1遇到sleep不會阻塞整個執行緒
    # 如果設定了timeout值,則意味著此處最多等待的秒,完成的協程返回值寫入到done中,未完成則寫到pending中
    # wait裡面要放可迭代物件
    done, pending = await asyncio.wait(task_list, timeout=None)
    print(done)
    print(pending)
    print('產生協程的函式執行結束')

if __name__ == '__main__':
    asyncio.run(create_coroutine())

[3]獲取協程返回值

在步驟[2]的基礎上使用async.gather()獲取返回值

import asyncio

# 定義協程功能函式
async def work(num):
    print('協程執行開始')
    await asyncio.sleep(1)  # 模擬非同步操作,比如網路請求
    print('協程執行結束')
    return num * 10

# 定義產生協程函式
async def create_coroutine():
    print('產生協程的函式執行開始')
    # 將work()得到的協程物件封裝到Task物件中,並立即新增到事件迴圈的任務列表中,等待事件迴圈
    task_list = [asyncio.create_task(work(i)) for i in range(1, 4)]

    # 引用了await之後,task1遇到sleep不會阻塞整個執行緒
    # 如果設定了timeout值,則意味著此處最多等待的秒,完成的協程返回值寫入到done中,未完成則寫到pending中
    # wait裡面要放可迭代物件
    done, pending = await asyncio.wait(task_list, timeout=None)
    response = await asyncio.gather(*task_list)
    print(response)
    print('產生協程的函式執行結束')

if __name__ == '__main__':
    asyncio.run(create_coroutine())

需要注意的是,以上程式碼即使不執行 done, pending = await asyncio.wait(task_list, timeout=None),也能獲取到返回值。

相關文章