Chapter4_并发网络请求

1、对比如下两段类似代码,解释为什么一个能并发,一个不能?

import asyncio
from util import async_timed, delay


@async_timed()
async def main() -> None:
    delay_times = [3, 3, 3]
    [await asyncio.create_task(delay(seconds)) for seconds in delay_times]

asyncio.run(main())

——————————————————————————————————————————————————————————————————————————————————————

import asyncio
from util import async_timed, delay


@async_timed()
async def main() -> None:
    delay_times = [3, 3, 3]
    tasks = [asyncio.create_task(delay(seconds)) for seconds in delay_times]
    [await task for task in tasks]

asyncio.run(main())

这两段代码的不同之处在于它们如何处理异步任务的创建和等待。让我们详细分析一下原因。

第一段代码

import asyncio
from util import async_timed, delay

@async_timed()
async def main() -> None:
    delay_times = [3, 3, 3]
    [await asyncio.create_task(delay(seconds)) for seconds in delay_times]

asyncio.run(main())
  • delay_times 是一个包含3个值 [3, 3, 3] 的列表。
  • 列表推导式 [await asyncio.create_task(delay(seconds)) for seconds in delay_times] 创建和等待每一个任务。
  • asyncio.create_task(delay(seconds)) 创建一个新的异步任务,但 await 立即等待这个任务完成。

原因:由于 await 在列表推导式中直接等待每一个任务完成,这导致这些任务是按顺序(串行)执行的。即,第一个任务完成后,第二个任务才开始,依此类推。因此,总的执行时间是所有任务执行时间的总和(大约 9 秒)。

第二段代码

import asyncio
from util import async_timed, delay

@async_timed()
async def main() -> None:
    delay_times = [3, 3, 3]
    tasks = [asyncio.create_task(delay(seconds)) for seconds in delay_times]
    [await task for task in tasks]

asyncio.run(main())
  • delay_times 是一个包含3个值 [3, 3, 3] 的列表。
  • 列表推导式 tasks = [asyncio.create_task(delay(seconds)) for seconds in delay_times] 创建了所有任务,但没有等待它们完成。
  • tasks 是一个包含三个 Task 对象的列表。
  • 列表推导式 [await task for task in tasks] 依次等待每一个任务完成。

原因:在这段代码中,所有任务在 tasks 列表创建时几乎同时启动,因为 asyncio.create_task 创建任务时不会阻塞。

然后,[await task for task in tasks] 依次等待每个任务的完成,但这些任务是并发执行的。总的执行时间是单个任务的执行时间(大约 3 秒),因为所有任务是同时进行的。

总结

  • 第一段代码:任务是串行执行的,因为每次 await 都会等待前一个任务完成后才启动下一个任务。
  • 第二段代码:任务是并发执行的,因为所有任务在创建时同时启动,然后等待所有任务完成。

视觉化解释

第一段代码(串行):

Time (seconds) →
0    3    6    9
|----|----|----|
T1        (T1)
       T2       (T2)
            T3       (T3)

第二段代码(并发):

Time (seconds) →
0    3
|----|
T1   (T1)
T2   (T2)
T3   (T3)

在并发情况下,所有任务同时启动,几乎在相同的时间完成。而在串行情况下,任务一个接一个地执行,导致总时间是每个任务时间的累加。

 

2、asyncio.gather()的return_exceptions参数的概念的理解

asyncio.gather()函数是用于并发运行多个协程的一个高级工具。其中的return_exceptions参数用于控制协程中产生的异常的处理方式。

概念 return_exceptions参数是一个布尔值,用于指定是否需要从asyncio.gather()返回由于协程引发的异常。

作用

return_exceptions=False(默认值)时,如果任何一个协程引发异常,asyncio.gather()就会抛出该异常,不再等待其他协程完成。【简单理解,有错就快速报,后面的中断不执行】

return_exceptions=True时,asyncio.gather()会等待所有协程完成,然后返回由结果和异常构成的列表。【简单理解,顾全大局,等全部执行完,在汇总结果】

通俗解释 假设你让朋友们帮你完成几项任务(协程),你可以选择:

  • return_exceptions=False(默认)时, 如果有人遇到问题(引发异常), 你立即得到通知(抛出异常), 其他人的工作也无法继续。
  • return_exceptions=True时, 即使有人遇到问题, 你也会等所有人都完成了, 然后你会收到一份报告, 详细列出每个人的结果,包括成功或失败(异常)。

举例说明

  1. return_exceptions=False(默认)
import asyncio

async def func1():
    return 1

async def func2():
    raise ValueError("Error occurred")

async def func3():
    return 3

async def main():
    try:
        results = await asyncio.gather(func1(), func2(), func3())
        print(results)
    except Exception as e:
        print(f"Exception raised: {e}")

asyncio.run(main())

输出:

Exception raised: Error occurred

由于func2()引发了ValueError异常,asyncio.gather()会立即抛出该异常,而不等待func3()的结果。

  1. return_exceptions=True
import asyncio

async def func1():
    return 1

async def func2():
    raise ValueError("Error occurred")

async def func3():
    return 3

async def main():
    results = await asyncio.gather(func1(), func2(), func3(), return_exceptions=True)
    print(results)

asyncio.run(main())

输出:

[1, ValueError('Error occurred'), 3]

由于return_exceptions=True,asyncio.gather()会等待所有协程完成,并将结果和异常一并返回。结果列表的第二个元素是func2()引发的ValueError异常。

通过这两个例子,可以看出return_exceptions参数对于控制协程中异常的处理方式非常有用。根据具体需求,开发者可以选择合适的值来满足自己的需求。

 

posted @ 2024-05-16 16:13  AlphaGeek  阅读(39)  评论(0)    收藏  举报