究竟是什么触发了 Asyncio 任务?

问题描述 投票:0回答:1

尝试理解 python

asyncio
来自
concurrent.futures
的多线程背景。这是示例脚本

#!/usr/bin/env python3
# encoding: utf-8
"""Sample script to test asyncio functionality."""

import asyncio
import logging
from time import sleep  # noqa
logging.basicConfig(format='%(asctime)s | %(levelname)s: %(message)s',
                    level=logging.INFO)

async def wait(i: int) -> None:
    """The main function to run asynchronously"""
    logging.info(msg=f'Entering wait {i}')
    await asyncio.sleep(5)
    logging.info(msg=f'Leaving wait {i}')  # This does not show because all pending tasks are SIGKILLed?

async def main() -> None:
    """The main."""
    [asyncio.create_task(
        coro=wait(i)) for i in range(10)]
    logging.info(msg='Created tasks, waiting before await.')
    sleep(5)  # This is meant to verify the tasks do not start by the create_task call.
    # What changes after the sleep command, i.e. here?
    # If the tasks did not start before the sleep, why would they start after the sleep?

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

技术堆栈(如果相关)是 Ubuntu 22.04 上的 python 3.10。这是我的终端输出。

2023-05-03 12:30:45,297 | INFO: Created tasks, waiting before await.
2023-05-03 12:30:50,302 | INFO: Entering wait 0
2023-05-03 12:30:50,304 | INFO: Entering wait 1
2023-05-03 12:30:50,304 | INFO: Entering wait 2
2023-05-03 12:30:50,304 | INFO: Entering wait 3
2023-05-03 12:30:50,304 | INFO: Entering wait 4
2023-05-03 12:30:50,304 | INFO: Entering wait 5
2023-05-03 12:30:50,304 | INFO: Entering wait 6
2023-05-03 12:30:50,304 | INFO: Entering wait 7
2023-05-03 12:30:50,304 | INFO: Entering wait 8
2023-05-03 12:30:50,304 | INFO: Entering wait 9

所以基于这个片段的两个相关问题。

  1. 这里究竟是什么触发了
    wait
    异步任务(进入和退出时只在控制台记录两行)?显然,创建任务并不能真正让它们运行,因为我在 main 中创建它们后等待了足够长的时间。甚至时间戳也显示它们是在 main 中的阻塞睡眠之后 运行的。 然而,就在 main 函数似乎完成睡眠并退出时,任务似乎被触发了。主线程不应该在此时退出吗?
  2. 退出日志从不打印(在代码中注释)。是不是主线程退出后才启动子进程,然后立即被杀死?
python multithreading subprocess python-asyncio
1个回答
1
投票
Q1:

来自

docs

asyncio.create_task(coro, *, name=None, context=None)

将 coro 协程包装到任务中并

schedule 它的执行。返回任务对象。 ...

任务在 get_running_loop() 返回的循环中执行,如果当前线程中没有正在运行的循环,则会引发 RuntimeError。

而这个计划任务执行发生在所谓的

Checkpoints - 引用另一个很棒的异步库,trio 的文档

检查点是两件事:

    Trio 检查取消的点。
  1. Trio 调度程序检查是否是切换到另一个任务的好时机。
(大部分时间)

await

 关键字是什么。

(这需要额外的解释才能完全理解 - 在 Q2 之后)


Q2:

为了说明

asyncio.create_task(~~)

之后依次发生的事情:

  1. 任务(任务

    不是子进程,但更粗略地类似于协程——一个可挂起的函数。)刚刚创建并排队等待执行。

  2. 但是在

    asyncio.create_task(~~)

     调用之后,您调用了 
    Synchronous IO Operation time.sleep(5)
     并挂起(阻塞)
    Main Thread 5 秒,因此,什么都没有发生。因此实际上什至没有任何任务开始。

  3. time.sleep(5)

    解锁后,预定的
    Tasks开始运行并到达检查点await asyncio.sleep(5)
    .

  4. 但是

    await

     中不再有 
    main()
     关键字允许调度程序切换到任务并运行。因此,在所有
    同步代码之后,main()
    在没有将控制权交还给调度程序的情况下完成,还没有创建的任务运行。

  5. 因为

    main()

    完成,现在调度程序运行很久以前安排的任务。

  6. 所有每个计划任务都运行并命中检查点

    await asyncio.sleep(5)

    ,将控制权交还给调度程序并等待在
    (current time + 5)
    重新安排。然后调度器运行下一个挂起的任务,重复这个过程。

  7. 所有任务都已运行并等待重新安排,但由于主任务

    main()

    已经退出,调度程序取消所有剩余任务。

  8. 现在没有什么是挂起的

    asyncio的循环停止并优雅地退出。

这可以通过更多的日志记录/打印来证明:

"""Sample script to test asyncio functionality.""" import asyncio import time async def wait(i: int) -> None: print(f"Task {i} started!") try: await asyncio.sleep(5) except (asyncio.CancelledError, KeyboardInterrupt): print(f"Task {i} canceled!") raise print(f"Task {i} done!") async def main() -> None: """The main.""" tasks = [asyncio.create_task(wait(i)) for i in range(10)] print("Blocking main thread for 5 sec!") time.sleep(5) print("End of main!") if __name__ == '__main__': asyncio.run(main=main()) print("asyncio loop stopped!")
输出:

Blocking main thread for 5 sec! End of main! Task 0 started! Task 1 started! Task 2 started! Task 3 started! Task 4 started! Task 5 started! Task 6 started! Task 7 started! Task 8 started! Task 9 started! Task 9 canceled! Task 4 canceled! Task 0 canceled! Task 1 canceled! Task 7 canceled! Task 2 canceled! Task 5 canceled! Task 8 canceled! Task 3 canceled! Task 6 canceled! asyncio loop stopped!
尝试按照上面的顺序进行操作!


补充说明

Application/Script 中通常的瓶颈不是 CPU,而是

IO。 IO包括写入、读取、等待等。访问硬盘或发送web请求是一个非常IO密集的工作负载,所有CPU都在等待。

说明一下,IO作品基本是这样的:

“嘿操作系统,请为我做这个 IO。完成后叫醒我。”

线程 1 进入休眠

一段时间后,操作系统打通线程 1

“你的 IO 操作已经完成,拿着这个回去工作。”

这就是为什么你在许多应用程序和框架中看到使用

Threading 来提高吞吐量,尽管存在 Global Interpreter Lock(GIL) 限制 python 代码在任何给定时间仅在 1 个线程中运行。

GIL在等待OS中断时被释放,让CPU做一些比等待更有用的事情。因此 CPU 可以同时做其他线程的工作。

但是线程不是免费的:尽管比

Process 轻得多,它仍然会产生相当大的开销来创建线程。

异步 IO 框架的主要思想是从同步代码中阻塞 IO 工作(读取、写入、等待等)中获益,但没有线程开销。

他们使用称为

Awaitables

 的东西而不是线程——这大致是某种协程,例如您之前使用的 futures


让我知道是否有任何错误 - 我认为在移动设备上写应该犯了很多错误 - 虽然仔细检查了!

© www.soinside.com 2019 - 2024. All rights reserved.