4、异步编程入门:事件循环、协程基础、async/await语法

异步编程,说白了就是让程序学会「边等边做」。

我刚开始写代码那会儿,遇到网络请求或者文件读写,程序就像卡住了一样。用户点个按钮,界面直接白屏几秒钟。嗯,这种体验谁受得了?

后来我明白了,问题出在「同步阻塞」上。今天我们就来聊聊怎么用异步编程解决这个问题。

4.1 事件循环:异步的发动机

事件循环是什么?你可以把它想象成一个永不休息的调度员。

这个调度员手里有个任务队列。它不停地问:「有没有任务要执行?有没有任务已经准备好了?」有,就拿过来执行。没有,就继续等。

我个人习惯把事件循环理解成一个 while 循环:

while (true) {
    // 检查有没有待执行的任务
    task = getNextTask();
    if (task) {
        execute(task);
    }
    // 检查有没有已经完成的 I/O 操作
    checkCompletedIO();
}

当然,真实的事件循环比这个复杂得多。但核心思想就一句话:别闲着,也别死等。

核心要点:事件循环是异步编程的底层机制。它负责调度所有异步任务,确保 CPU 在等待 I/O 时不会空转。

我在项目中遇到过一个问题:某个服务偶尔会卡死几秒钟。排查了半天,发现是有人在主线程里做了个耗时的同步计算,直接把事件循环给堵住了。你想想看,调度员都被你绑住了,其他任务还怎么执行?

4.2 协程:轻量级的线程

协程这个概念,很多人一听就头大。其实没那么复杂。

线程是操作系统调度的,协程是程序员自己调度的。说白了,协程就是可以暂停和恢复的函数。

举个例子:

async def fetch_data(url):
    print(f"开始请求:{url}")
    # 这里会暂停,等数据回来再继续
    data = await http_get(url)
    print(f"数据回来了:{len(data)} 字节")
    return data

看到那个 await 了吗?它就是协程的「暂停键」。程序执行到这里,会主动让出 CPU,去做别的事情。等数据准备好了,再回来继续执行。

我记得刚开始用协程时,总觉得它和线程差不多。后来踩了个坑才明白区别:

特性 线程 协程
调度方式 操作系统抢占式 程序主动让出
切换开销 较大(微秒级) 极小(纳秒级)
内存占用 每个线程几 MB 每个协程几 KB
适用场景 CPU 密集型 I/O 密集型

我的建议:如果你的程序主要是在等网络、等磁盘、等数据库,用协程。如果是做大量计算,老老实实用多线程。

4.3 async/await:写异步代码像写同步代码

在 async/await 出现之前,写异步代码是什么体验?回调地狱!

// 回调地狱示例
getUser(id, function(user) {
    getOrders(user.id, function(orders) {
        getDetails(orders[0].id, function(details) {
            // 已经嵌套了3层...
        });
    });
});

这种代码,维护起来简直要命。后来 async/await 出现了,世界清净了。

async/await 其实就是语法糖。它让你用写同步代码的方式,去写异步代码:

async def process_user(user_id):
    # 看起来像是同步调用,实际上是异步的
    user = await get_user(user_id)
    orders = await get_orders(user.id)
    details = await get_details(orders[0].id)
    return details

你看,代码变平了,逻辑清晰了。这就是 async/await 的魅力。

注意:await 只能在 async 函数内部使用。我曾经见过有人直接在全局作用域写 await,结果报错。记住,await 必须待在 async 的「保护伞」下。

4.4 实战:用协程并发请求

光说不练假把式。我们来看一个实际例子:

import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
        "https://api.example.com/data1",
        "https://api.example.com/data2",
        "https://api.example.com/data3",
    ]
    
    async with aiohttp.ClientSession() as session:
        # 创建3个协程任务
        tasks = [fetch_url(session, url) for url in urls]
        # 并发执行,等待所有完成
        results = await asyncio.gather(*tasks)
        
    for url, data in zip(urls, results):
        print(f"{url}: {len(data)} 字节")

# 运行
asyncio.run(main())

这段代码同时发起了3个网络请求。如果是同步方式,需要等第一个回来才能发第二个。用协程,3个请求几乎同时发出,总耗时只取决于最慢的那个。

我曾经用这个模式优化过一个数据采集服务。原来同步采集100个接口要30秒,改成协程并发后,3秒就搞定了。性能提升了10倍。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 别在协程里做 CPU 密集型操作——协程切换靠的是主动让出,如果你在协程里做大量计算,事件循环会被卡住,其他协程就得不到执行。
  • 小心共享状态——协程虽然是单线程,但多个协程同时访问同一个变量,还是会有竞态条件。该加锁还是要加锁。
  • 不要混用同步和异步库——比如在协程里用 requests 库(同步),会阻塞整个事件循环。用 aiohttp 或者 httpx 的异步模式。

一句话总结:事件循环是调度员,协程是轻量任务,async/await 是写异步代码的优雅方式。三者配合,让你的程序在 I/O 密集型场景下飞起来。

异步编程核心逻辑 事件循环 永不休息的调度员 任务队列 待执行的协程 协程 可暂停的函数 工作流程: 1. 事件循环从任务队列中取出一个协程 2. 执行协程直到遇到 await(暂停点) 3. 协程让出 CPU,事件循环去执行其他协程 4. 当 I/O 完成,协程被重新放回任务队列 5. 事件循环继续执行该协程,从暂停点恢复 核心思想:别闲着,也别死等

小技巧:刚开始学异步编程,别急着理解所有细节。先记住「await 就是暂停键」这个比喻。用多了,自然就懂了。

好了,异步编程入门就聊到这里。记住,事件循环、协程、async/await 这三兄弟,是你写出高性能 I/O 程序的利器。下次遇到网络请求慢、文件读写卡的问题,试试用异步的方式去解决。

可可精品资料库