Day 39:【99天精通Python】异步编程 (AsyncIO) 上篇 - 协程的魔法

前言

欢迎来到第39天!

在前面的课程中,我们学习了多线程。线程虽然好用,但它是由操作系统负责调度的。操作系统很忙,它要在几千个线程之间来回切换(Context Switch),这需要消耗不少资源。当并发量达到上万级别时,线程切换的开销就会拖垮系统。

协程 (Coroutine) 是一种比线程更轻量级的存在。

  • 线程:操作系统决定什么时候切换,你无法控制。
  • 协程程序自己决定什么时候切换(“我等数据的时候,你先干别的”)。

Python 3.4 引入了 asyncio 库,Python 3.5 引入了 asyncawait 关键字,标志着 Python 进入了原生异步编程时代。这是高性能网络服务器(如 FastAPI, Tornado)的基石。

本节内容:

  • 同步 (Sync) vs 异步 (Async)
  • asyncawait 关键字
  • 事件循环 (Event Loop)
  • 运行协程:asyncio.run()
  • 并发执行:asyncio.gather()
  • 实战:体验"光速"睡眠

一、同步 vs 异步

1.1 同步 (Synchronous)

代码从上到下依次执行。如果第一行卡住了(比如下载文件),第二行就得干等。

import time

def task(name):
    time.sleep(1) # 阻塞 1 秒
    print(f"{name} 完成")

task("A")
task("B")
# 总耗时: 2 秒

1.2 异步 (Asynchronous)

当第一行卡住(等待 I/O)时,程序会自动挂起它,去执行第二行。等第一行结果回来了,再恢复执行。

# 伪代码逻辑
await task("A") # 你先下着,我去干别的
await task("B") # 你也下着
# 总耗时: 约 1 秒 (因为是同时等的)

二、Hello AsyncIO

2.1 定义协程 (async def)

使用 async def 定义的函数不再是普通函数,而是一个协程函数。调用它不会立即执行,而是返回一个协程对象。

import asyncio

async def say_hello():
    print("Hello")
    return "World"

# 直接调用不会执行打印!
# coroutine = say_hello()
# print(coroutine) # <coroutine object ...>

2.2 运行协程 (asyncio.run)

要让协程跑起来,必须把它扔进事件循环 (Event Loop)
Python 3.7+ 提供了最简单的入口:asyncio.run()

import asyncio

async def main():
    print("开始")
    # await 后面必须跟一个可等待对象 (Coroutine, Task, Future)
    # 这里不能用 time.sleep,要用 asyncio.sleep
    await asyncio.sleep(1) 
    print("结束")

if __name__ == '__main__':
    asyncio.run(main())

注意asyncio.sleep(1) 是非阻塞的睡眠,而 time.sleep(1) 是阻塞的。在协程中千万别用 time.sleep,否则整个程序都会卡死!


三、并发执行:asyncio.gather

如果我们按顺序写两个 await,它们还是串行的。

async def main():
    await task(1) # 等它做完
    await task(2) # 再做这个
    # 依然是串行,没体现出异步优势

要实现并发,我们需要告诉事件循环:“把这几个任务一起安排了!”。使用 asyncio.gather()

实战对比:同步 vs 异步

我们模拟烤面包(2秒)和煮咖啡(3秒)。

import asyncio
import time

# --- 异步任务 ---
async def make_toast():
    print("开始烤面包...")
    await asyncio.sleep(2) # 模拟耗时 I/O
    print("面包烤好了!")
    return "Toast"

async def make_coffee():
    print("开始煮咖啡...")
    await asyncio.sleep(3)
    print("咖啡煮好了!")
    return "Coffee"

async def main():
    start = time.time()
    
    print("--- 早餐开始 ---")
    # 并发执行两个任务
    # gather 会等待所有任务完成,并按顺序返回结果列表
    results = await asyncio.gather(make_toast(), make_coffee())
    
    end = time.time()
    print(f"--- 早餐结束,耗时: {end - start:.2f} 秒 ---")
    print(f"结果: {results}")

if __name__ == '__main__':
    asyncio.run(main())

运行结果

--- 早餐开始 ---
开始烤面包...
开始煮咖啡...
(过了2秒)
面包烤好了!
(又过1秒)
咖啡煮好了!
--- 早餐结束,耗时: 3.01 秒 ---
结果: ['Toast', 'Coffee']

如果用同步方式,需要 2+3=5 秒。异步方式只用了 3 秒(取决于最长的那个任务)。


四、深入理解:await 到底在干嘛?

await 关键字的作用是:

  1. 挂起当前协程(暂停执行)。
  2. 交出控制权给事件循环,让它去调度其他协程。
  3. 等待后面的对象(如 sleep 或网络请求)返回结果。
  4. 恢复执行。

这就像你在餐厅点菜:

  • await 点菜():你告诉服务员要什么,服务员去厨房下单(交出控制权)。
  • 服务员去服务其他桌的客人(调度其他任务)。
  • 厨房做好了(IO完成),服务员把菜端给你(恢复执行)。

五、常见的坑 (必看)

坑1:在协程里写了阻塞代码

这是新手最容易犯的错。

async def bad_coroutine():
    print("开始")
    # time.sleep 是阻塞的!它会霸占 CPU,不让出控制权。
    # 导致整个线程卡住,其他协程也跑不了。
    import time
    time.sleep(5) 
    print("结束")

原则:在 async def 函数里,所有耗时操作都必须是异步的(支持 await 的),比如 asyncio.sleep,或者异步库(aiohttp, aiomysql)。不能用普通的 requests, time.sleep

坑2:忘记写 await

async def main():
    # 这样写只会创建一个协程对象,但不会执行它!
    # RuntimeWarning: coroutine 'xxx' was never awaited
    asyncio.sleep(1) 
    
    # 正确写法
    await asyncio.sleep(1)

六、小结

异步编程 AsyncIO

核心概念

关键字

运行模式

Event Loop (调度中心)

Coroutine (协程对象)

非阻塞 I/O

async def (定义)

await (挂起/等待)

asyncio.run() (入口)

asyncio.gather() (并发)

关键要点

  1. 协程是单线程并发,靠的是"合作式调度"(自己主动让出 CPU)。
  2. async def 定义协程,await 调度协程。
  3. asyncio.gather 是并发执行的神器。
  4. 千万别在协程里用阻塞代码(如 time.sleep, requests),否则一核有难,八核围观。

七、课后作业

  1. 异步倒计时:编写一个协程 countdown(name, n),每秒打印一次倒计时(n, n-1, … 1)。并发运行 3 个倒计时任务(比如 "A"倒数3秒,"B"倒数5秒)。
  2. 效率对比:编写一个普通的函数 sync_cal()(使用 time.sleep(1))和一个协程 async_cal()(使用 asyncio.sleep(1))。分别循环调用它们 5 次(同步循环 vs gather 并发),对比总耗时。
  3. 思考题:为什么计算密集型任务(如算圆周率)不适合用 asyncio?(提示:回顾一下 GIL 和单线程的本质)。

下节预告

Day 40:异步编程 (AsyncIO) 下篇 - aiohttp - 既然不能用 requests,那在协程里怎么发网络请求?我们将学习 Python 最强的异步网络库 aiohttp,体验每秒几千次请求的快感!


系列导航

  • 上一篇:Day 38 - 线程池与进程池
  • 下一篇:Day 40 - 异步编程AsyncIO下(待更新)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐