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 事件循环:异步调度器

光有协程还不够,得有个东西来调度它们。这就是事件循环。

事件循环的本质是一个无限循环,反复做三件事:

  1. 检查:轮询哪些IO事件已经就绪
  2. 获取:从就绪队列里取出协程或回调
  3. 执行:运行那个协程直到它再次暂停或完成

在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 非阻塞、通知驱动 协程 可暂停/恢复的函数 事件循环 调度器 + 多路复用 协作流程 1. 协程发起IO 2. 事件循环注册 3. IO就绪唤醒 协程遇到await → 暂停 → 事件循环接管 → IO完成后通知事件循环 → 恢复协程继续执行

从图里可以看得很清楚:协程是代码层面的“暂停点”,事件循环负责调度这些暂停点,而异步IO是底层的通信机制,让事件循环知道什么时候可以唤醒协程。

曾经有个同事,把asyncio.sleep()替换成了time.sleep(),结果整个事件循环被卡住,所有协程都动不了。记住:在协程里,任何阻塞操作都必须用异步版本,否则就是灾难。

1.5 总结几个关键认知

  • 异步不是并行:单协程的代码还是顺序执行的,但多个协程可以“穿插”执行,提高IO吞吐。
  • 协程切换开销极低:大约是线程切换的百分之一,所以才能支持上万的并发。
  • 事件循环是心脏:所有协程的调度、IO事件的监听都靠它。理解事件循环,就理解了异步的魂。

好了,基础就聊这么多。记住这三者的关系,后面的课程才能跟得上。下一轮我们深入异步缓冲区的写入模型,看看为什么瓶颈在那儿,以及怎么优化。