Skip to content

Python 异步编程

并发模型

了解异步,目前大概有以下并发模型

OS 线程:即我们常说的多线程

事件驱动模型:似乎就是利用回调函数

异步协程:今天的内容

区别

异步:目标是用"单线程"处理多任务,避免 IO 等待浪费 CPU

协程:协程是实现异步编程的一种高效技术手段

异步 和 多线程 是两个概念

异步: I/O 密集型场景(网络请求、文件读写)

多进程/多线程:CPU 密集型任务(如科学计算,视频编解码)

有这样一个场景:

当厨师需要煮肉时,他开始煮,就不要等着了,而是去炒菜,节约时间,等肉煮好,再去取肉

这个场景的关键是:

肉可以被锅来煮,而不是你来煮

肉就是 “网络请求、文件读写”

如果是网络请求,那肉就是陈皮,只需要等着,文件读写,这是一个确定的事情

这些操作,都可以让操作系统代为完成,所以它可以异步

如果你的肉,是科学计算,那你的肉只能自己来煮,系统不知道你想煮成什么样子

所以异步编程的本质:

本质上,还是有多个线程

你单独自己一个线程,你调用 fopen,只是 C 库帮你封装了系统 API,比如 ReadFile,到时候是你的程序陷入内核态,你自己去读文件,你这个时候只能读文件,你回不到你的用户态去做下一步

异步,只是这些高级语言,或者高级库。它们把系统线程封装好了

在你的视角下,你永远只管你自己,当读写时,你只需告诉库,你需要进行读写,库/语言会让操作系统去读,读完了告诉你,因此操作系统(可能)会启动另一个系统线程(这比你自己用多线程启动一个用户线程去读更快),来帮你读,你不占用 CPU,但是系统线程占用 CPU 来读(如果是网络,就等着)

你只需要写 async/await,感觉所有事情在一个线程里发生。但最终都是由硬件并行处理的,你的 CPU 线程不需要参与等待。

为什么说"单线程"?

因为控制流是单线程的

  1. 你写的所有 async 函数都在同一个线程里执行
  2. 执行权通过 await 显式传递
  3. 没有数据竞争问题(不需要锁)
  4. 代码顺序容易理解

即使底层使用了线程池,从你的代码视角看,还是逻辑上的单线程

以前的解决方案是 C/C++:

核心思想是:

“当异步操作完成时,系统自动调用你预先注册的函数”

这导致很多问题:

1、难以看到堆栈 2、控制流不好管理 3、生命周期不好管理

Python 异步

Python 3.5+asyncawait 作为关键字正式引入

简单理解:

async 这个基本上是放在 def (函数声明)前面

而 await 是放在执行时间不确定的函数调用前面

如果你想理解 Python 的异步,则必须先了解协程

协程 Coroutine,实际上是一个线程。

假设在炒菜,多线程是雇佣多个厨师,但是厨师之间因为灶台的竞争而打架,需要加锁,并由操作系统来协调,这样开销很大

而协程,则是由一个厨师完成。当厨师需要煮肉时,他开始煮,就不要等着了,而是去炒菜,节约时间

单个线程中实现高并发(Concurrency),尤其是处理 I/O 密集型任务(如网络请求、读写文件)

而协程在等待时,它只会“暂停”(await),并立即把执行权让给同一个线程中的其他协程。

这个线程(厨师)从头到尾都在忙碌,CPU 利用率极高

C++ 类比

async def ≈ C++20 的 coroutine 函数

而不是 C++ 11 thread 那一套

线程同步复杂 (锁/条件变量)

协程写法接近同步代码

返回值问题:

当一个 async 函数被调用,返回的是一个“协程对象”,你可以理解为一个承诺

python
async def my_function():
    return "Hello, World!"
# 调用 async 函数
result = my_process()  # 这不会执行函数,而是返回一个协程对象
print(type(result))    # <class 'coroutine'>
print(result)          # <coroutine object my_function at 0x...>

只能在 async def 函数内部使用 await

await 的使用会“逐层向外传递”

python
async def task1():
    print("开始任务 1")
    time.sleep(2)  # 这里会阻塞整个事件循环!
    await asyncio.sleep(2) # 这里让出控制权,其他协程可以运行
    print("完成任务 1")
    return "任务 1 结果"

这个是不一样的。

一个是自己去睡,一个是自己没睡,让别人睡,过一会他醒了告诉自己他已经起床

前者相当于是 视频编解码,后者才是 网络请求

当然,这些异步编程库里大多都有线程池,所以你的自己睡,也可以封装为别人睡

并发并行

异步 = 并发,一般网络服务器都说并发量很大

多线程,如果是一个 CPU 模拟的,则是并发,如果是多个核心,则是并行

web 服务器不使用每个请求一个线程,是因为请求量很大了,线程太重量级了,而且线程需要管理锁的问题