阿森毛不多
Published on

考虑 Python asyncio 异步编程之前,应该注意哪些问题?

Authors
  • avatar
    Name
    浩森 Hansen
    Twitter

并行和并发

编程中的函数,大家都熟悉。一个函数可以表示一段机器执行过程,而 async 异步函数,指的就是可以被调度器异步调度的函数。

在说异步之前,先搞清楚并行、并发这个两比较容易混淆的概念:

  • 并行:指的是在同一时间内,多个任务同时一起运行。
  • 并发:指的是不同的任务之间没有明确的先后执行关系。任务可以交替先后执行,也可以同时一起执行。

也就是说,现在有两个任务 A 和 B,如果他们之间没有明确的先后顺序(例如没有要求 A 跑完后 B 才能开始),那么 A、B 就可以并发执行。

在单线程执行的情况下,并发的 A、B 可以交替运行,例如 A 先跑一半,然后再跑 B,最后再把 A 跑完。

如果在多线程情况下,A、B 可以同时运行,速度翻一倍。就像马路上并排行驶的两辆车,同时向一个方向齐头并进。

也许你已经发现了并发和并行的区别了:

以上情况,A 和 B 是两个可并发的任务:在单线程中 A、B 交替运行,但不并行;多线程下,A、B 并行了。

所以,我认为并发和并行最本质的区别是:【并发】是描述任务(函数)性质的,而【并行】是描述执行情况的。

从概念上来看,并行的前提是任务可以并发,所以并行要满足的条件更多。

在 Python asyncio 中的异步编程,一般指的是并发编程。

明确以上概念,就可以继续讨论了。

异步函数-协程

一段示例代码:

import asyncio
import aiohttp

# 定义一个异步函数模拟网络请求
async def fetch_data(url, delay):
    print(f"⏳ 开始请求 {url} (延迟 {delay}s)")

    # 模拟网络延迟(非阻塞等待)
    await asyncio.sleep(delay)

    # 实际项目中这里可以替换为真正的网络请求
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            content = await response.text()
            print(f"✅ {url} 请求完成,响应长度: {len(content)} 字节")
            return content

用 async def 声明的函数,就是一个异步函数(协程),代表了一个可并发执行的任务。

一个典型的 async 函数里面,往往都有 await 标记。这就像高速公路上的路牌,标记在这个地方协程进入等待状态,并让出执行权。

async 函数,不是一个普通的 Python 函数,在执行时 Python 会对其做一些封装。如果我们直接调用它获得的返回值是一个 Coroutine 对象。无法直接使用。

那么应该如何使用 async 函数呢?

执行器和事件循环

async 函数必须使用特定的调度器调用。最简单的,就是 asyncio.run()。

asyncio.run() 会创建一个事件循环,并且在事件循环中执行 async 函数。当然 asyncio 还提供了很多其他方法,可以在更细的颗粒度上操作调度器。

不论怎么样,你只需要记住:async 异步函数必须在一个事件循环内下才能正常执行即可。

异步编程的局限性

异步编程确实让并发开发变得容易,在实际使用的过程中,还是会有不小的局限性:

首先是三方库之间的兼容性问题,像 FastAPI 和 APScheduler 都支持异步编程的事件循环调度。但是两个库一起使用的时候,他们会把所有任务放在同一个事件循环中运行。

这就会造成很多奇怪的问题,例如 APScheduler 的定时任务不执行,或者执行的长任务占用太久,导致 FastAPI 的 Router 无响应。这么一来,两个模块相互影响,违背了低耦合原则。

其次是异步编程的生态完整性问题,一旦使用异步编程,那么就要保证所有使用的第三方库都应该支持异步调用。例如数据库、http 请求访问等。如果调用的库不支持异步方法,那么就要用多线程来异步化。

一旦用到多线程,编程就变得复杂了。原本使用异步编程是为了写并发函数更方便,但是这样一来,反而把事情复杂化了,那么我为什么不一开始就用多线程来完成异步呢?

总结

异步编程是个挺有用的技术,但是就像其他技术一样,都有其适用范围。有优势的同时,也有其局限性。

总的来说,一旦使用异步,那么整个项目中的生态,不论是上游中间件还是下游三方库,都要异步化。这里的工作量还是挺大的。

如果更多考虑通用的兼容性,那么异步编程可能并不适合你。