1. 异步编程基础
各位好,我是老刘。咱们这个《异步缓冲区写入瓶颈优化》课程,得先从最底层说起。异步编程不是新东西,但做IO密集型系统的时候,它几乎是唯一的解药。
1.1 什么是异步IO
传统程序处理网络请求或文件读写,都是同步方式。说白了就是:调个接口,然后死等。线程或进程卡在系统调用上,啥也干不了。这叫阻塞IO。
异步IO不一样。你发起一个读操作,立刻返回一个“凭证”,然后去做别的事情。等数据准备好了,系统会通知你。我在项目中遇到过一台接口机,每秒要处理两万个WebSocket消息。如果用同步模型,光线程切换就能把CPU吃光。后来改成异步,单进程就扛住了。
核心区别:同步是“人等你”,异步是“你等人——人转去做别的事”。
下面这个表可以帮你快速对照:
| 维度 | 同步IO | 异步IO |
|---|---|---|
| 调用返回 | 等待结果就绪才返回 | 立即返回,稍后通知 |
| 资源占用 | 阻塞期间线程/进程挂起 | 几乎不占资源,可复用 |
| 吞吐能力 | 线程数 = 并发数,上限低 | 少量线程处理大量并发 |
| 典型场景 | 脚本、小型工具 | 高并发网络服务、实时系统 |
你想想看,假如一个线程处理一个连接,一万个连接就是一万个线程。线程上下文切换的代价,足以让CPU趴在桌上喘气。异步IO正是为了解决这个问题而生的。
1.2 协程:轻量级线程
异步IO虽然好,但直接写回调代码很容易陷入“回调地狱”。
所以就有了协程。协程本质上是一个可以暂停和恢复的函数。它不像线程那样由操作系统调度,而是程序自己主动让出CPU。协程的切换开销极小,一个进程里跑几十万个协程都没问题。
我个人习惯把协程理解为“用户态线程”。函数执行到await(或yield)时,它就暂停,把执行权交还给调度器。等IO事件到了,调度器再把它唤醒,从暂停的地方继续跑。
Python里,用async def声明的函数就是一个协程函数。调用它不执行函数体,而是返回一个协程对象。
我曾经调试过一个异步爬虫,协程数量开到十万,内存占用还不到200MB。换成线程试试?早就OOM了。
1.3 事件循环:异步调度器
光有协程还不够,得有个东西来调度它们。这就是事件循环。
事件循环的本质是一个无限循环,反复做三件事:
- 检查:轮询哪些IO事件已经就绪
- 获取:从就绪队列里取出协程或回调
- 执行:运行那个协程直到它再次暂停或完成
在Python中,asyncio.run()会帮你创建事件循环,把主协程丢进去跑。所有底层IO都由select/epoll/kqueue这样的多路复用器完成。
嗯,这里要注意:事件循环不能同时跑多个(至少标准库是这样)。所以计算密集型的任务别塞进协程里,不然会把循环堵死。
import asyncio
async def fetch_data(idx):
print(f"任务{idx}开始")
await asyncio.sleep(1) # 模拟IO等待
print(f"任务{idx}完成")
async def main():
# 同时调度三个协程
tasks = [fetch_data(i) for i in range(3)]
await asyncio.gather(*tasks)
asyncio.run(main())
这段代码跑起来,三个任务几乎同时开始,1秒后同时结束。要是同步写,得花3秒。这就是异步的魅力。
1.4 三者怎么配合?
异步IO、协程、事件循环是三位一体的关系。我把它画成一张图:
从图里可以看得很清楚:协程是代码层面的“暂停点”,事件循环负责调度这些暂停点,而异步IO是底层的通信机制,让事件循环知道什么时候可以唤醒协程。
曾经有个同事,把asyncio.sleep()替换成了time.sleep(),结果整个事件循环被卡住,所有协程都动不了。记住:在协程里,任何阻塞操作都必须用异步版本,否则就是灾难。
1.5 总结几个关键认知
- 异步不是并行:单协程的代码还是顺序执行的,但多个协程可以“穿插”执行,提高IO吞吐。
- 协程切换开销极低:大约是线程切换的百分之一,所以才能支持上万的并发。
- 事件循环是心脏:所有协程的调度、IO事件的监听都靠它。理解事件循环,就理解了异步的魂。
好了,基础就聊这么多。记住这三者的关系,后面的课程才能跟得上。下一轮我们深入异步缓冲区的写入模型,看看为什么瓶颈在那儿,以及怎么优化。