Python 异步编程
并发模型
了解异步,目前大概有以下并发模型
OS 线程:即我们常说的多线程
事件驱动模型:似乎就是利用回调函数
异步协程:今天的内容
区别
异步:目标是用"单线程"处理多任务,避免 IO 等待浪费 CPU
协程:协程是实现异步编程的一种高效技术手段
异步 和 多线程 是两个概念
异步: I/O 密集型场景(网络请求、文件读写)
多进程/多线程:CPU 密集型任务(如科学计算,视频编解码)
有这样一个场景:
当厨师需要煮肉时,他开始煮,就不要等着了,而是去炒菜,节约时间,等肉煮好,再去取肉
这个场景的关键是:
肉可以被锅来煮,而不是你来煮
肉就是 “网络请求、文件读写”
如果是网络请求,那肉就是陈皮,只需要等着,文件读写,这是一个确定的事情
这些操作,都可以让操作系统代为完成,所以它可以异步
如果你的肉,是科学计算,那你的肉只能自己来煮,系统不知道你想煮成什么样子
所以异步编程的本质:
本质上,还是有多个线程
你单独自己一个线程,你调用 fopen,只是 C 库帮你封装了系统 API,比如 ReadFile,到时候是你的程序陷入内核态,你自己去读文件,你这个时候只能读文件,你回不到你的用户态去做下一步
异步,只是这些高级语言,或者高级库。它们把系统线程封装好了
在你的视角下,你永远只管你自己,当读写时,你只需告诉库,你需要进行读写,库/语言会让操作系统去读,读完了告诉你,因此操作系统(可能)会启动另一个系统线程(这比你自己用多线程启动一个用户线程去读更快),来帮你读,你不占用 CPU,但是系统线程占用 CPU 来读(如果是网络,就等着)
你只需要写 async/await,感觉所有事情在一个线程里发生。但最终都是由硬件并行处理的,你的 CPU 线程不需要参与等待。
为什么说"单线程"?
因为控制流是单线程的:
- 你写的所有 async 函数都在同一个线程里执行
- 执行权通过 await 显式传递
- 没有数据竞争问题(不需要锁)
- 代码顺序容易理解
即使底层使用了线程池,从你的代码视角看,还是逻辑上的单线程。
以前的解决方案是 C/C++:
核心思想是:
“当异步操作完成时,系统自动调用你预先注册的函数”。
这导致很多问题:
1、难以看到堆栈 2、控制流不好管理 3、生命周期不好管理
Python 异步
Python 3.5+:async 和 await 作为关键字正式引入
简单理解:
async 这个基本上是放在 def (函数声明)前面
而 await 是放在执行时间不确定的函数调用前面
如果你想理解 Python 的异步,则必须先了解协程
协程 Coroutine,实际上是一个线程。
假设在炒菜,多线程是雇佣多个厨师,但是厨师之间因为灶台的竞争而打架,需要加锁,并由操作系统来协调,这样开销很大
而协程,则是由一个厨师完成。当厨师需要煮肉时,他开始煮,就不要等着了,而是去炒菜,节约时间
在单个线程中实现高并发(Concurrency),尤其是处理 I/O 密集型任务(如网络请求、读写文件)
而协程在等待时,它只会“暂停”(await),并立即把执行权让给同一个线程中的其他协程。
这个线程(厨师)从头到尾都在忙碌,CPU 利用率极高。
C++ 类比
async def ≈ C++20 的 coroutine 函数
而不是 C++ 11 thread 那一套
线程同步复杂 (锁/条件变量)
协程写法接近同步代码
返回值问题:
当一个 async 函数被调用,返回的是一个“协程对象”,你可以理解为一个承诺
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 的使用会“逐层向外传递”
async def task1():
print("开始任务 1")
time.sleep(2) # 这里会阻塞整个事件循环!
await asyncio.sleep(2) # 这里让出控制权,其他协程可以运行
print("完成任务 1")
return "任务 1 结果"这个是不一样的。
一个是自己去睡,一个是自己没睡,让别人睡,过一会他醒了告诉自己他已经起床
前者相当于是 视频编解码,后者才是 网络请求
当然,这些异步编程库里大多都有线程池,所以你的自己睡,也可以封装为别人睡
并发并行
异步 = 并发,一般网络服务器都说并发量很大
多线程,如果是一个 CPU 模拟的,则是并发,如果是多个核心,则是并行
web 服务器不使用每个请求一个线程,是因为请求量很大了,线程太重量级了,而且线程需要管理锁的问题