目錄
0.基礎
1.休眠--asyncio.sleep
2.屏蔽取消--asyncio.shield
3.超时--asyncio.wait_for
4.简单的等待--wait
5.迭代可等待对象的完成--asyncio.as_completed
首頁 後端開發 Python教學 Python Asyncio函式庫之asyncio.task常用函式有哪些

Python Asyncio函式庫之asyncio.task常用函式有哪些

May 12, 2023 pm 07:49 PM
python asyncio

0.基礎

在《Python Asyncio調度原理》中介紹了Asyncio的兩種排程基本單位,HandlerTimeHandler,他們只能被loop.call_xx函數調用,開發者從表面上不知道他們的存在,他們和loop.call_xx屬於事件循環的基礎功能,但是這些操作都屬於單一操作,需要開發者自己寫程式把他們的操作給串連起來。而在《Python的可等待物件在Asyncio的作用》中介紹了協程鏈的發起者asyncio.Task能透過loop.call_soon跟事件循環進行交互,並串聯整個協程鏈中可等待物件以及安排可等待物件的運行。不過對於loop.call_atloop.call_later仍需要開發者透過asyncio.Future來把Timehandler的執行結果與asyncio.Task給串聯起來,例如休眠一秒的程式碼實作:

import asyncio


async def main():
    loop = asyncio.get_event_loop()
    f = asyncio.Future()

    def _on_complete():
        f.set_result(True)

    loop.call_later(1, _on_complete)
    return await f


if __name__ == "__main__":
    import time
    s_t = time.time()
    asyncio.run(main())
    print(time.time() - s_t)
登入後複製

這段程式碼中asyncio.Future執行的是類似容器的功能,自己本身會接受各種狀態,並把自己的狀態同步給管理目前協程鏈的asyncio.Task,使asyncio.Task能管理其他類型的操作。在asyncio.tasks模組中的所有功能函數的原理也差不多,他們接受的參數基本上是可等待對象,然後透過asyncio.Futurte作為容器來同步呼叫端和可等待物件間的狀態,也可以透過其他的一些方法把asyncio.Task的狀態同步給等待物件。

1.休眠--asyncio.sleep

asyncio.sleep是一個常用的方法,開發者透過它可以很方便的讓協程休眠設定的時間,它本身也非常簡單,它的源碼如下:

@types.coroutine
def __sleep0():
    yield


async def sleep(delay, result=None):
    """Coroutine that completes after a given time (in seconds)."""
    if delay <= 0:
        await __sleep0()
        return result

    loop = events.get_running_loop()
    future = loop.create_future()
    h = loop.call_later(delay,
                        futures._set_result_unless_cancelled,
                        future, result)
    try:
        return await future
    finally:
        h.cancel()
登入後複製

透過原始碼可以發現當設定的休眠時間等於小於0的時候,sleep只執行了yield,並不會執行其他邏輯,而在值大於0時會建立一個Future對象,接著就一直等待,直到Future物件被loop.call_later控制結束時才傳回結果值。

要注意的是,當asyncio.sleep在值為0時,sleep執行yield可以讓Task.__step 感知而讓出控制權,這是最小的讓出當前協程控制權的方法,所以我們在編寫涉及到CPU比較多的時候或者消耗時間較長的函數時可以通過asyncio. sleep(0)來主動讓出控制權,如下:

import asyncio

async def demo() -> None:
    for index, i in enumerate(range(10000)):
        if index % 100 == 0:
            await asyncio.sleep(0)
        ...  # 假设这里的代码占用过多的CPU时间
登入後複製

在這個例子中每循環100次就讓出控制權,以減少對其他協程的影響。

2.屏蔽取消--asyncio.shield

asyncio.shield可以保護一個可等待物件被取消,或者說是防止協程鏈上的取消傳播到被asyncio.shield託管的可等待對象,但是呼叫可等待對象的cancel方法仍然可以取消可等待對象的運行,如下範例:

import asyncio


async def sub(f):
    await asyncio.shield(f)


async def main():
    f1 = asyncio.Future()
    f2 = asyncio.Future()
    sub1 = asyncio.create_task(sub(f1))
    sub2 = asyncio.create_task(sub(f2))
    f1.cancel()
    sub2.cancel()
    await asyncio.sleep(0)  # 确保已经取消完成
    print("f1 future run success:", f1.done())
    print("f2 future run success:", f2.done())
    print("sub1 future run result:", sub1.done())
    print("sub2 future run result:", sub2.done())

asyncio.run(main())

# >>> future run success: True
# >>> future run success: False
# >>> sub1 future run result: True
# >>> sub2 future run result: True
登入後複製

其中f1, f2都在main函數中創建, 然後同時被sub函數包裹,並透過asyncio.create_task 在後台非同步運行並分別傳回sub1sub2兩個Future對應著sub函數的執行情況。接著分別取消f1sub2的執行,並把f1,f2,sub1, sub2是否為done印出來,可以發現f1,sub1,sub2的狀態都為done(被取消也認為是done),而f2則還在運作中。

在文章《Python的可等待對像在Asyncio的作用》中說過,一條協程鍊是由asyncio.Task牽頭組成的,後續的所有成功和異常都會在這條鏈上傳播,而取消本質上就是一種異常,所以也可以在協程鏈上傳播。而shield為了杜絕運行的可等待物件收到協程鏈的異常傳播又能讓協程鏈知道可等待物件的執行結果,會先讓可等待物件在另外一條協程鏈運行,然後建立一個容器接到原來鏈上,並在可等待物件執行完成的時候把結果告訴容器,由容器把結果傳播到原有的協程鏈上,對應的源碼如下:

def shield(arg):
    # 如果是Coro,则需要包装成future
    inner = _ensure_future(arg)
    if inner.done():
        # 如果已经完成,就不需要被处理了
        return inner
    loop = futures._get_loop(inner)
    # 创建一个future容器
    outer = loop.create_future()

    def _inner_done_callback(inner):
        if outer.cancelled():
            if not inner.cancelled():
                # 如果容器已经被取消,而自己没被取消且已经完成,则手动获取下结果,方便被回收
                inner.exception()
            return

        if inner.cancelled():
            # 如果自己被取消,则把取消通过容器传播到协程链上
            outer.cancel()
        else:
            # 自己已经完成且容器未完成,把自己的结果或者异常通过替身传播到协程链上
            exc = inner.exception()
            if exc is not None:
                outer.set_exception(exc)
            else:
                outer.set_result(inner.result())


    def _outer_done_callback(outer):
        if not inner.done():
            inner.remove_done_callback(_inner_done_callback)

    # 添加回调,在执行成功或被取消时通知对方
    inner.add_done_callback(_inner_done_callback)
    outer.add_done_callback(_outer_done_callback)
    return outer
登入後複製

通过源码可以发现shield被调用的时候(假设驱动调用shieldTask名为main.Task),会先通过_ensure_future辅助函数创建一个Task(other.Task)在后台异步运行可等待对象,驱动可等待对象的运行,由于是新的Task驱动着可等待对象的执行,所以main.Task的任何状态不会传播到当前的可等待对象。 接着创建一个Future容器,并在other.TaskFuture容器挂上完成的回调使他们在完成的时候都能通知到对方,最后返回Future容器给main.Task,使main.Task能够间接的知道可等待对象的运行结果,如下图:

Python Asyncio函式庫之asyncio.task常用函式有哪些

不过Future容器完成的回调只是把托管可等待对象的other.Task回调给移除了,导致main.Task的状态不会同步到other.Task中(图中Future通知可等待对象aws的通道是不通的),进而不会影响到托管的可等待对象。 而other.Task完成的回调会把任何状态同步到Future中,进而影响到main.Task

3.超时--asyncio.wait_for

asyncio.wait_for可以托管可等待对象,直到可等待对象完成,不过可等待对象在设定的时间内还没执行完成时会被直接取消执行并抛出asyncio.TimeoutError异常。 它的运行原理综合了上面的asyncio.shieldasyncio.sleep,它一样会为可等待对象创建一个Future容器,并在容器上挂了一个超时的回调和可等待对象执行结束的回调,接着就等待容器执行结束。 不过在了解asyncio.wait_for之前,先了解他用到的两个辅助函数_cancel_and_wait_release_waiter,他们的源码如下:

def _release_waiter(waiter, *args):
    if not waiter.done():
        waiter.set_result(None)

async def _cancel_and_wait(fut, loop):
    waiter = loop.create_future()
    cb = functools.partial(_release_waiter, waiter)
    fut.add_done_callback(cb)

    try:
        fut.cancel()
        await waiter
    finally:
        fut.remove_done_callback(cb)
登入後複製

可以看出源码比较简单,他们的作用都是为了确保可等待对象能完全执行结束才返回,其中_release_waiter是确保可等待对象一定被设置为执行结束,而_cancel_and_wait是为了确保能等到可等待对象被取消且完整结束时才返回。

可等待对象的cancel方法可以认为是异步的,调用后需要等事件循环再次调用可等待对象时,可等待对象才会被取消。而_cancel_and_wait通过一个容器来规避这个问题,使取消这个操作变为同步的,这个方法在某些开发场景经常被使用,如果不是私有API就更好了。

接下来就可以通过wait_for的源码了解他的执行逻辑了,源码如下:

async def wait_for(fut, timeout):
    loop = events.get_running_loop()

    if timeout is None:
        return await fut

    if timeout <= 0:
        # 当超时的值小于等于0时就意味着想马上得到结果
        
        fut = ensure_future(fut, loop=loop)

        if fut.done():
            # 如果执行完成就返回可等待对象的数据
            return fut.result()
        # 取消可等待对象并等待
        await _cancel_and_wait(fut, loop=loop)
        # 如果被_cancel_and_wait取消,那么会抛出CancelledError异常,这时候把它转为超时异常
        try:
            return fut.result()
        except exceptions.CancelledError as exc:
            raise exceptions.TimeoutError() from exc

    # 初始化一个Future,只有在超时和完成时才会变为done
    waiter = loop.create_future()
    timeout_handle = loop.call_later(timeout, _release_waiter, waiter)
    cb = functools.partial(_release_waiter, waiter)

    fut = ensure_future(fut, loop=loop)
    fut.add_done_callback(cb)

    try:
        try:
            await waiter
        except exceptions.CancelledError:
            # 此时是asyncio.Task被取消,并把取消传播到waiter
            if fut.done():
                return fut.result()
            else:
                # 如果任务被取消了,那么需要确保任务没有被执行才返回
                fut.remove_done_callback(cb)
                await _cancel_and_wait(fut, loop=loop)
                raise
        # 计时结束或者是执行完毕的情况
        if fut.done():
            # 执行完毕,返回对应的值
            return fut.result()
        else:
            # 计时结束,清理资源,并抛出异常
            fut.remove_done_callback(cb)
            # 如果任务被取消了,那么需要确保任务没有被执行才返回
            await _cancel_and_wait(fut, loop=loop)
            # 如果被_cancel_and_wait取消,那么会抛出CancelledError异常,这时候把它转为超时异常
            try:
                return fut.result()
            except exceptions.CancelledError as exc:
                raise exceptions.TimeoutError() from exc
    finally:
        timeout_handle.cancel()
登入後複製

wait_for的源码为了兼容各种情况,代码复杂度比较高,同时超时参数小于等于0跟大于0的逻辑是一样的,分开写只是为了避免在小于等于0时创建了一些额外的对象,在精简了一些asyncio.Task传播异常给waiter的逻辑后,wait_for的执行逻辑如下图:

Python Asyncio函式庫之asyncio.task常用函式有哪些

fut为可等待对象,timeout为超时时间

可以看到wait_for的主要逻辑是先创建一个名为waiter的容器,接着通过loop.call_later指定在多少时间后释放容器,然后再通过ensure_future使另一个asyncio.Task来托管可等待对象,并安排执行完毕的时候释放容器,再等待waiter容器的执行直到被释放。当容器被释放的时候再判断可等待对象是否执行完毕,如果执行完毕了就直接返回,否则抛出超时异常。

4.简单的等待--wait

asyncio.wait用于等待一批可等待对象,当有一个可等待对象执行完成或者出现异常的时候才会返回数据(具体还是要看return_when指定的条件,默认为所有等待对象结束或取消时才返回),需要注意的是wait虽然支持timeout参数,但是在超时的试试不会取消可等待对象,也不会抛出超时的异常,只会把完成的可等待对象放在完成的集合,把未完成的可等待对象放在未完成的集合并返回,如下代码:

import asyncio


async def main():
    return await asyncio.wait(
        {asyncio.create_task(asyncio.sleep(1))},
        timeout=0.5
    )


if __name__ == "__main__":
    asyncio.run(main())
登入後複製

这段代码可以正常的运作,不会抛出超时错,不过还要注意的是在后续版本中asyncio.wait只支持Task对象,如果想要传入的是coroFuture对象,则需要开发者自己手动转换。 wait的逻辑与wait_for类似,源码如下:

async def _wait(fs, timeout, return_when, loop):
    assert fs, &#39;Set of Futures is empty.&#39;
    waiter = loop.create_future()
    timeout_handle = None
    if timeout is not None:
        # 定义一个time handler,在timeout秒后通过`_release_waiter`完成.
        timeout_handle = loop.call_later(timeout, _release_waiter, waiter)
    counter = len(fs)

    def _on_completion(f):
        # 每个可等待对象执行完成的回调
        nonlocal counter
        counter -= 1
        if (counter <= 0 or
            return_when == FIRST_COMPLETED or
            return_when == FIRST_EXCEPTION and
             (not f.cancelled() and f.exception() is not None)
        ):
            # 如果所有任务执行完成,或者是第一个完成或者是第一个抛出异常时,
            # 意味着执行完成,需要取消time handler,并标记为完成
            if timeout_handle is not None:
                timeout_handle.cancel()
            if not waiter.done():
                waiter.set_result(None)
    # 为每个可等待对象添加回调
    for f in fs:
        f.add_done_callback(_on_completion)

    try:
        # 等待替身执行完成
        await waiter
    finally:
        # 取消time handler并移除回调(因为cancel是异步的)
        if timeout_handle is not None:
            timeout_handle.cancel()
        for f in fs:
            f.remove_done_callback(_on_completion)

    # 处理并返回done和pending,其中done代表完成,pending代表执行中。
    done, pending = set(), set()
    for f in fs:
        if f.done():
            done.add(f)
        else:
            pending.add(f)
    return done, pending
登入後複製

可以看到wait_for的复杂度没有wait高,而且可以看到asyncio.wait是等waiter这个容器执行完并移除可等待对象上面的_on_completion回调后才把可等待对象按照是否完成区分到donepending两个集合,这样的准确度比在_on_completion高一些,但是如果开发者在处理集合时触发一些异步操作也可能导致pending集合中的部分可等待对象变为完成的,如下代码:

import asyncio


async def main():
    f_list = [asyncio.Future() for _ in range(10)]
    done, pending = await asyncio.wait(f_list, timeout=1)
    print(len(done), len(pending))
    print([i for i in pending if i.done()])
    f_list[1].set_result(True)
    print([i for i in pending if i.done()])


if __name__ == "__main__":
    asyncio.run(main())
# >>> 0 10
# >>> []
# >>> [<Future finished result=True>]
登入後複製

通过输出可以发现,在asyncio.wait执行完毕后,pending中的完成的元素只有0个,而在后续强制为其中的一个Future设置数据后,pending中完成的元素有1个了。

5.迭代可等待对象的完成--asyncio.as_completed

asyncio.wait的机制是只要被触发就会返回,其他尚未完成的可等待对象需要开发者自己在处理,而asyncio.as_completed可以确保每个可等待对象完成返回数据或者超时时抛出异常,使用方法如下:

import asyncio


async def sub(i):
    await asyncio.sleep(i)
    return i


async def main():
    for f in asyncio.as_completed([sub(i) for i in range(5)], timeout=3):
        print(await f)


if __name__ == "__main__":
    asyncio.run(main())
# >>> 0
# >>> 1
# >>> 2
# >>> Traceback (most recent call last):
#       File "/home/so1n/github/demo_project/demo.py", line 18, in <module>
#         asyncio.run(main())
#       File "/usr/lib/python3.7/asyncio/runners.py", line 43, in run
#         return loop.run_until_complete(main)
#       File "/usr/lib/python3.7/asyncio/base_events.py", line 584, in run_until_complete
#         return future.result()
#       File "/home/so1n/github/demo_project/demo.py", line 14, in main
#         print(await f)
#       File "/usr/lib/python3.7/asyncio/tasks.py", line 532, in _wait_for_one
#         raise futures.TimeoutError
#     concurrent.futures._base.TimeoutError
登入後複製

该程序并发执行5个协程,其中执行最久的时间是5秒,而as_completed设置的超时为3秒。通过输出可以发现,每当一个可等待对象执行结束时就会把数据抛出来,当超时则会抛出超时错误。为了能达每有一个可等待对象就返回一次数据的效果,as_completed通过一个队列来维护数据的返回,它的源码如下:

def as_completed(fs, *, timeout=None):
    from .queues import Queue  # Import here to avoid circular import problem.
    done = Queue()

    loop = events._get_event_loop()
    todo = {ensure_future(f, loop=loop) for f in set(fs)}
    timeout_handle = None

    def _on_timeout():
        # 超时时调用,需要注意的是,失败时结果为空,所以要推送一个空的数据到队列中
        # 在消费者发现元素为空时抛出错误
        for f in todo:
            f.remove_done_callback(_on_completion)
            done.put_nowait(None)  # Queue a dummy value for _wait_for_one().
        todo.clear()  # Can&#39;t do todo.remove(f) in the loop.

    def _on_completion(f):
        # 如果成功,就把Future推送到队列中,消费者可以通过Future获取到结果
        if not todo:
            return  # _on_timeout() was here first.
        todo.remove(f)
        done.put_nowait(f)
        if not todo and timeout_handle is not None:
            timeout_handle.cancel()

    async def _wait_for_one():
        f = await done.get()
        if f is None:
            # 如果元素为空,则证明已经超时了,要抛出异常
            raise exceptions.TimeoutError
        return f.result()

    for f in todo:
        f.add_done_callback(_on_completion)
    if todo and timeout is not None:
        timeout_handle = loop.call_later(timeout, _on_timeout)
    # 通过生成器语法返回协程函数,该协程函数可以获取最近完成的可等待对象的结果
    for _ in range(len(todo)):
        yield _wait_for_one()
登入後複製

通过源码可以发现可等待对象就像生产者一样,执行结束的时候就会把结果投递给队列,同时as_completed会迭代跟可等待对象的数量一样的_wait_for_one协程函数,供开发者消费数据。不过需要注意的是as_completed在超时的时候,并不会取消尚未完成的可等待对象,他们会变为不可控的状态,在某些时候会造成内存溢出,如下示例代码:

import asyncio
import random


async def sub():
    # 一半的几率会被set一个值并返回,一半的几率会卡死
    f = asyncio.Future()
    if random.choice([0, 1]) == 0:
        f.set_result(None)
    return await f


async def main():
    try:
        for f in asyncio.as_completed([sub() for i in range(5)], timeout=1):
            print(await f)
    except asyncio.TimeoutError:
        # 忽略超时
        pass
    # 统计未完成的sub任务
    cnt = 0
    for i in asyncio.all_tasks():
        if i._coro.__name__ == sub.__name__:
            cnt += 1
    print("runing task by name sub:", cnt)


if __name__ == "__main__":
    asyncio.run(main())
# >>> None
# >>> None
# >>> None
# >>> runing task by name sub: 2
登入後複製

通过结果(由于采用随机,结果可能不一样)可以发现,sub成功执行完成的数量有3个(输出None),而在as_completed触发超时后仍有两个sub在执行中,这时的两个sub成为无人管理的可等待对象,除非开发者通过asyncio.all_tasks去找到他并清理掉,否则这几个可等待对象会一直伴随着程序运行,这很容易造成内存溢出。

以上是Python Asyncio函式庫之asyncio.task常用函式有哪些的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1665
14
CakePHP 教程
1424
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

sublime怎麼運行代碼python sublime怎麼運行代碼python Apr 16, 2025 am 08:48 AM

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

Golang vs. Python:性能和可伸縮性 Golang vs. Python:性能和可伸縮性 Apr 19, 2025 am 12:18 AM

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。

vscode在哪寫代碼 vscode在哪寫代碼 Apr 15, 2025 pm 09:54 PM

在 Visual Studio Code(VSCode)中編寫代碼簡單易行,只需安裝 VSCode、創建項目、選擇語言、創建文件、編寫代碼、保存並運行即可。 VSCode 的優點包括跨平台、免費開源、強大功能、擴展豐富,以及輕量快速。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

See all articles