Skip to main content
 首页 » 编程设计

python之在什么情况下 ensure_future 不能真正开始 future

2025年05月04日14sky-heaven

我正在尝试在子进程中使用 wget 在 Python 中异步下载文件。我的代码如下所示:

async def download(url, filename): 
    wget = await asyncio.create_subprocess_exec( 
        'wget', url, 
        'O', filename 
    ) 
    await wget.wait() 
 
 
def main(url): 
    loop = asyncio.get_event_loop() 
    future = asyncio.ensure_future(download(url, 'test.zip'), loop=loop) 
    print("Downloading..") 
    time.sleep(15) 
    print("Still downloading...") 
    loop.run_until_complete(future) 
    loop.close() 

我正在尝试做的是见证打印“正在下载..”,然后在 15 秒后显示“仍在下载...”,同时文件下载已经开始。我实际看到的是文件的下载仅在代码命中 loop.run_until_complete(future)

我的理解是 asyncio.ensure_future 应该开始运行 download 协程的代码,但显然我遗漏了一些东西。

请您参考如下方法:

当传递协程时,asyncio.ensure_future 将其转换为任务 - 一种知道如何驱动协程的特殊 future - 并将其排入事件循环中。 “Enqueue”意味着协程内部的代码将由调度协程的正在运行的事件循环执行。如果事件循环没有运行,那么协程也没有机会运行。通过调用 loop.run_forever() 或 loop.run_until_complete(some_future) 告知循环运行。在问题中,事件循环仅在调用 time.sleep() 之后 开始,因此下载开始延迟 15 秒。

time.sleep 不应在运行 asyncio 事件循环的线程中被调用。正确的休眠方式是使用 asyncio.sleep,它在等待时将控制权交给事件循环。 asyncio.sleep 返回一个可以提交给事件循环或从协程中等待的 future :

# ... definition of download omitted ... 
 
async def report(): 
    print("Downloading..") 
    await asyncio.sleep(15) 
    print("Still downloading...") 
 
def main(url): 
    loop = asyncio.get_event_loop() 
    dltask = loop.create_task(download(url, 'test.zip')) 
    loop.create_task(report()) 
    loop.run_until_complete(dltask) 
    loop.close() 

上面的代码有一个不同的问题。当下载时间短于 15 秒时,会导致打印Task was destroyed but it is pending! 警告。问题是当下载任务完成并且循环关闭时,report 任务从未被取消,它只是被放弃了。发生这种情况通常表示存在错误或对 asyncio 的工作方式有误解,因此 asyncio 会用警告标记它。

消除警告的明显方法是显式取消 report 协程的任务,但生成的代码最终变得冗长且不是很优雅。一个更简单和更短的修复是更改 report 以等待下载任务,指定显示“仍在下载...”消息的超时:

async def dl_and_report(dltask): 
    print("Downloading..") 
    try: 
        await asyncio.wait_for(asyncio.shield(dltask), 15) 
    except asyncio.TimeoutError: 
        print("Still downloading...") 
        # assuming we want the download to continue; otherwise 
        # remove the shield(), and dltask will be canceled 
        await dltask 
 
def main(url): 
    loop = asyncio.get_event_loop() 
    dltask = loop.create_task(download(url, 'test.zip')) 
    loop.run_until_complete(dl_and_report(dltask)) 
    loop.close()