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 请求访问等。如果调用的库不支持异步方法,那么就要用多线程来异步化

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

总结

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

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

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