最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

淺析Python 3.11以下如何優(yōu)雅地實現(xiàn)自動取消任務

 更新時間:2026年04月26日 08:34:50   作者:站大爺IP  
這篇文章主要為大家詳細介紹了Python 3.11以下如何優(yōu)雅地實現(xiàn)自動取消任務,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下

?小李今天又遇到了煩心事。

他寫了一個數據處理腳本,要調用外部API獲取一萬個用戶的信息。每個請求大概要等2秒。他不想干等著,所以用了asyncio,并發(fā)發(fā)出去50個請求。

跑了三分鐘,腳本卡住了。

不是死鎖,就是單純的慢——有幾個API服務不穩(wěn)定,響應要半分鐘。他想:“能不能設定一個超時時間,比如5秒,超過5秒就不等了,直接跳過?”

這個需求太常見了。不管是網絡請求、數據庫查詢還是復雜的計算任務,你總不希望它無限期地卡下去。就像點外賣,等了45分鐘還沒到,你肯定想取消訂單,換一家點。

問題在于,Python的異步超時機制,在3.11之前有一個不大不小的“坑”,如果你不注意,任務可能根本取消不掉。

一個“取消失敗”的真實案例

先來看一段代碼,它在Python 3.10上運行:

import asyncio

async def quick_task():
    # 這個任務瞬間完成
    return "done"

async def wrapper():
    # 設置30秒超時,但實際任務1秒就完成
    return await asyncio.wait_for(quick_task(), timeout=30)

async def main():
    task = asyncio.create_task(wrapper())
    await asyncio.sleep(0)  # 讓任務開始執(zhí)行
    task.cancel()           # 手動取消
    
    try:
        await task
    except asyncio.CancelledError:
        print("任務被取消了")
    else:
        print("任務沒有被取消!")

asyncio.run(main())

在Python 3.8上運行,輸出的是“任務被取消了”。但在Python 3.9或3.10上運行,輸出的卻是“任務沒有被取消!”

什么情況?明明調用了cancel(),為什么任務沒被取消?

真相藏在wait_for的源碼里

要理解這個問題,得看看asyncio.wait_for這個函數到底在干什么。

wait_for的作用是:給一個任務設置一個超時時間,如果超時了就取消它。但這里有一個細節(jié)——如果任務在超時之前就已經完成了,而在完成的那一瞬間你恰好發(fā)起了取消請求,wait_for會怎么處理?

Python 3.8的做法是:不管任務完沒完成,只要收到了取消信號,就拋出一個CancelledError。

Python 3.9及之后的做法是:先檢查一下任務是不是已經完成了。如果已經完成了,“取消”就沒有意義了,直接返回任務的結果,不拋異常。

聽起來3.9之后的邏輯更合理對吧?畢竟任務都做完了,還取消什么?

但在并發(fā)環(huán)境下,問題就出在這個“先檢查”上。

想象一下這個時序:

  1. 任務在wait_for里等著
  2. 你調用task.cancel()
  3. wait_for收到取消信號
  4. 它問:任務完成了嗎?如果回答“完成了”,它就直接返回結果,把取消信號吞掉

這個“任務完成了嗎”的判斷,在多線程/多任務的并發(fā)環(huán)境下,會因為時序問題出現(xiàn)誤判。任務實際上還沒有真正完成,只是處于“即將完成”的狀態(tài),wait_for就可能把它當成“已完成”,從而無視你的取消請求。

這不是bug,而是設計上的一種取舍。但這個取舍導致了一個后果:在Python 3.11以下的版本中,取消操作并不是100%可靠的。

那怎么辦?三個靠譜的解決方案

既然wait_for靠不住,那就自己動手。

方案一:自己封裝一個“靠譜版的wait_for”

思路很簡單:不依賴wait_for的取消機制,而是自己用asyncio.create_task()加一個超時的“看門狗”。

import asyncio

async def cancellable_wait_for(coro, timeout):
    """
    一個更可靠的wait_for版本,確保取消信號不會被吞掉
    """
    task = asyncio.create_task(coro)
    
    try:
        # 等待任務完成,或者超時
        return await asyncio.wait_for(task, timeout=timeout)
    except asyncio.TimeoutError:
        # 超時了,取消任務
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            # 確保取消信號被傳播出去
            pass
        raise  # 重新拋出TimeoutError
    except asyncio.CancelledError:
        # 外部取消了,把取消信號傳遞給內部任務
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass
        raise  # 重新拋出CancelledError

async def my_task():
    try:
        await asyncio.sleep(10)
        return "完成"
    except asyncio.CancelledError:
        print("內部任務被取消了")
        raise

async def main():
    task = asyncio.create_task(cancellable_wait_for(my_task(), timeout=5))
    await asyncio.sleep(2)
    task.cancel()
    
    try:
        await task
    except asyncio.CancelledError:
        print("外部:確實被取消了")

asyncio.run(main())

這個方案的要點是:不管什么情況,只要外部取消或者超時,都強制取消內部任務,然后把取消信號往上拋。不會被“任務已完成”這種假象迷惑。

方案二:用第三方庫quattro的CancelScope

Python 3.11之后,官方推出了asyncio.timeout(),終于有了靠譜的超時機制。那3.11以下怎么辦?

有一個第三方庫叫quattro,它在3.11以下版本中實現(xiàn)了類似的功能。

# pip install quattro
import asyncio
from quattro import move_on_after

async def main():
    with move_on_after(5) as scope:  # 5秒超時
        result = await some_slow_operation()
        print(result)
    
    if scope.cancelled_caught:
        print("超時被取消了,但程序繼續(xù)運行")

asyncio.run(main())

quattroCancelScope比官方的更靈活:它是普通的上下文管理器(不需要async with),可以手動調用scope.cancel()提前取消,還可以查詢是否被取消了。

如果你在項目中需要同時支持Python 3.9、3.10和3.11,quattro是個不錯的選擇。代碼寫一遍,在所有版本上行為一致。

方案三:最底層的做法——自己用Event和超時循環(huán)

如果不想引入第三方庫,也嫌自己封裝太麻煩,還有一個最樸素的辦法:不用wait_for,自己用asyncio.Event加上循環(huán)檢查。

import asyncio

async def cancellable_operation(timeout):
    """
    在操作內部主動檢查超時和取消信號
    """
    start = asyncio.get_event_loop().time()
    
    # 假設這是一系列的小步驟
    for step in range(10):
        # 檢查是否超時
        if asyncio.get_event_loop().time() - start > timeout:
            raise asyncio.TimeoutError()
        
        # 檢查是否被取消(通過捕獲取消信號)
        try:
            await asyncio.sleep(0.5)  # 模擬一個小步驟
        except asyncio.CancelledError:
            # 做清理工作
            print("收到取消信號,正在清理...")
            raise  # 重新拋出,讓上層知道被取消了
        
        print(f"完成步驟 {step}")
    
    return "全部完成"

async def main():
    task = asyncio.create_task(cancellable_operation(timeout=3))
    await asyncio.sleep(2)
    task.cancel()
    
    try:
        result = await task
        print(result)
    except asyncio.TimeoutError:
        print("超時了")
    except asyncio.CancelledError:
        print("被取消了")

asyncio.run(main())

這個方案的優(yōu)點是:你完全掌控取消邏輯。缺點是:你得在操作內部主動插入檢查點。如果你的操作是一大塊無法分割的同步代碼,這個方法就不太適用了。

那同步代碼怎么辦?線程池里怎么取消?

上面聊的都是異步代碼(async/await)。但小李的腳本里,requests.get()是同步的,它不響應asyncio的取消信號。

這種情況下,task.cancel()根本沒用。因為取消信號只在await的地方才能被處理,而同步代碼里沒有await。

解決方案是:用loop.run_in_executor把同步代碼扔到線程池里,然后用一個threading.Event來手動控制停止。

import asyncio
import threading
import time

def blocking_task(stop_event):
    """
    這是一個會阻塞的同步函數
    它定期檢查stop_event,如果被設置了就主動退出
    """
    print("同步任務開始")
    for i in range(30):
        if stop_event.is_set():
            print("收到停止信號,主動退出")
            return "被停止了"
        print(f"工作中... {i}")
        time.sleep(1)  # 模擬耗時操作
    return "正常完成"

async def main():
    stop_event = threading.Event()
    loop = asyncio.get_running_loop()
    
    # 把同步任務扔到線程池里執(zhí)行
    task = loop.run_in_executor(None, blocking_task, stop_event)
    
    # 5秒后發(fā)送停止信號
    await asyncio.sleep(5)
    stop_event.set()
    
    # 等待任務結束
    result = await task
    print(f"結果: {result}")

asyncio.run(main())

關鍵點:同步任務本身必須主動檢查stop_event,不能指望Python幫你“強行”取消。強行殺線程在Python里是不安全的,也不被推薦。

那如果用多進程呢?

如果你面對的是CPU密集型的任務,線程也幫不了你——Python的GIL鎖會讓多線程在計算任務上毫無優(yōu)勢。這時候考慮用ProcessPoolExecutor

import asyncio
from concurrent.futures import ProcessPoolExecutor
import time

def cpu_intensive_task():
    """模擬一個費CPU的大活兒"""
    total = 0
    for i in range(100000000):
        total += i
        # 每1000萬次檢查一次,但進程間通信復雜,先忽略細節(jié)
        if i % 10000000 == 0:
            print(f"計算到 {i}")
    return total

async def main():
    loop = asyncio.get_running_loop()
    with ProcessPoolExecutor(max_workers=1) as pool:
        # 在子進程中執(zhí)行
        task = loop.run_in_executor(pool, cpu_intensive_task)
        
        # 等待3秒
        await asyncio.sleep(3)
        print("3秒到了,但子進程不會自己停...")
        # 注意:ProcessPoolExecutor沒有優(yōu)雅的取消方法
        # 只能shutdown(wait=False)但會有warning
        # 或者terminate,但不推薦

多進程的取消更麻煩。進程不像線程,你不能優(yōu)雅地通知它“停下來”。常見的做法是:在子進程里也放一個檢查循環(huán),通過進程間通信(比如multiprocessing.Event或隊列)來傳遞停止信號。

總結一下,到底選哪個方案?

給你一張決策表:

你的場景推薦方案
純異步代碼,Python 3.11+直接用官方asyncio.timeout()
純異步代碼,Python 3.10及以下quattroCancelScope,或自己封裝cancellable_wait_for
同步阻塞代碼(如requests)線程池 + threading.Event手動輪詢檢查
CPU密集型代碼多進程 + 進程間通信信號,或用concurrent.futures的超時(有限支持)
不想改現(xiàn)有代碼,只想加個保護asyncio.wait_for,但要接受它可能偶爾失效

小李最后選擇了方案一:自己封裝了一個cancellable_wait_for,用了半小時寫完,以后所有異步任務都走這個函數,再也沒出現(xiàn)過“取消不掉”的情況。

他說了一句大實話:“官方的不靠譜,就自己寫一個。反正常用的功能就那么幾個,封裝一次到處用,不虧。”

到此這篇關于淺析Python 3.11以下如何優(yōu)雅地實現(xiàn)自動取消任務的文章就介紹到這了,更多相關Python自動取消任務內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論

深圳市| 敦化市| 浦江县| 阳西县| 华池县| 东丰县| 新田县| 平顺县| 沙坪坝区| 元朗区| 大石桥市| 文登市| 全南县| 萨迦县| 澄城县| 静宁县| 临洮县| 沂水县| 龙山县| 梁山县| 连城县| 澳门| 万山特区| 磴口县| 扶余县| 鄄城县| 南汇区| 临泽县| 威宁| 泰和县| 夹江县| 上林县| 南投县| 长泰县| 丽江市| 灵宝市| 安阳市| 得荣县| 平乡县| 齐齐哈尔市| 特克斯县|