欢迎光临
我们一直在努力

【学习笔记】Python异步编程:如何让程序“一心多用”还不乱套?

一、异步编程解决了什么问题?

同步编程中,遇到 I/O 等待(如网络请求、文件读写、数据库操作)时,CPU 会“傻等”着时间过去,这段时间本可以用来处理其他任务。

异步编程的核心价值就是:在等待 I/O 的时候去做别的事,用单线程实现高并发。通俗来说:异步编程就是让单线程也能干多件事,不等白等,高效非阻塞。

二、核心概念与用法

2.1 async def — 定义协程函数

使用 async def 定义的是异步函数(协程函数),调用后不会立即执行,而是返回一个协程对象。

async def my_coroutine():
await asyncio.sleep(1)
return "Done"

coro = my_coroutine()
print(type(coro)) # <class 'coroutine'>

协程对象和普通函数的区别在于:协程对象拥有 __await__ 方法,使其可被 await。

2.2 await — 挂起并让出控制权

await 关键字只能在异步函数内部使用,它的作用是:

  • 挂起当前协程,保存执行状态(局部变量、指令指针)
  • 让出控制权给事件循环
  • 事件循环可以调度其他任务执行
  • 当被等待的对象完成后,事件循环恢复该协程继续执行

⚠️ 关键理解:await 不是“等结果”,而是“注册回调 + 暂停自己”。

2.3 可等待对象(Awaitable)

await 后面跟的对象必须是 Awaitable(实现了 __await__ 方法),主要包括:

  • 协程对象(async def 调用返回)
  • Task(对协程的封装)
  • Future(异步操作结果的占位符)
  • 自定义实现了 __await__ 的类的实例

2.4 正确执行异步函数

必须通过事件循环来调度执行,最简单的方式是使用 asyncio.run():

import asyncio

async def async_task():
print("异步函数执行了")

asyncio.run(async_task()) # 正确方式

直接调用协程函数不会执行,还会触发 RuntimeWarning: coroutine was never awaited。

三、底层原理

3.1 协程的本质:从生成器到协程

Python 的协程建立在生成器(Generator) 基础之上:

  • 协程对象和生成器对象一样,拥有 send()、throw()、close() 方法
  • 内部维护着自己的栈帧和局部变量状态
  • 每次遇到 await,协程把当前执行上下文保存进协程对象,然后返回控制权给事件循环
  • 当被等待的对象就绪后,事件循环调用该协程的 send(None),从上次暂停处继续执行

协程经历了三个阶段演进:生成器 → @asyncio.coroutine 装饰器 → async/await 原生语法。

3.2 事件循环(Event Loop)— 异步编程的心脏

事件循环是 asyncio 的核心调度器,负责:

  • 任务调度:决定哪个协程在何时执行
  • I/O 监听:监听 socket、文件描述符等事件(基于 selector/epoll/kqueue/IOCP)
  • 回调执行:在条件满足时执行注册的回调函数
  • 定时器管理:处理延时任务
  • 工作流程:

    事件循环启动 → 从就绪队列取任务执行 → 遇到 await →
    协程挂起,放入等待队列 → 继续处理就绪队列中的下一个任务 →
    I/O 完成/定时器到期 → 事件循环唤醒对应协程 → 恢复执行

    事件循环本质上是一个 while 循环,不断检查哪些协程已就绪,然后重新投入执行队列。

    关键特性:

    • 单线程:默认在主线程运行,无多线程开销
    • 非抢占式:协程必须主动 await 才能让出执行权
    • 如果协程中有长时间计算而不 await,会阻塞整个事件循环

    3.3 Future — 异步结果的占位符

    Future 代表一个异步操作的最终结果,是一个可等待对象。

    import asyncio

    async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future() # 推荐用 loop.create_future() 创建

    def set_result():
    fut.set_result("异步操作完成!")

    loop.call_later(2, set_result)
    result = await fut # 等待 Future 完成
    print(result)

    核心要点:

    • asyncio.Future 不应该被直接实例化,应使用 loop.create_future() 工厂函数
    • Future 不是线程安全的
    • 可以设置结果(set_result)或异常(set_exception)
    • 可以绑定回调(add_done_callback)

    3.4 Task — 协程的封装与调度

    Task 是 Future 的子类,用于封装协程并将其提交给事件循环调度。

    关键区别:

    • Future:只是一个结果的占位符,本身不驱动任何操作
    • Task:主动运行一个协程,并在协程完成时设置 Future 的结果

    创建 Task:

    python

    task = asyncio.create_task(my_coroutine()) # 推荐方式

    create_task 会立即将协程提交到事件循环,返回一个 Task 对象。

    Task 状态转换:

    • Pending:任务创建后尚未执行
    • Running:事件循环正在运行该任务
    • Done:协程完成,结果已设置

    3.5 并发执行多个任务

    asyncio.gather() — 并发执行并收集结果

    async def main():
    tasks = [
    asyncio.create_task(task("任务1", 2)),
    asyncio.create_task(task("任务2", 1)),
    asyncio.create_task(task("任务3", 3))
    ]
    results = await asyncio.gather(*tasks) # 等待全部完成,返回结果列表

    gather 关注的是收集结果,按传入顺序返回结果列表。如果某个任务抛出异常,默认立即传播该异常。

    asyncio.wait() — 更底层的并发控制

    wait 更底层,返回 (done, pending) 两个集合。支持更细粒度的控制(如 FIRST_COMPLETED、超时等)。

    对比总结

    特性

    gather

    wait

    返回值

    结果列表

    (done, pending)

    元组

    异常处理

    默认立即传播

    异常不自动抛出,需手动处理

    使用场景

    需要所有结果

    需要精细控制任务状态

    串行 vs 并发

    ⚠️ 常见误区:await coro1(); await coro2() 是串行执行,不是并发。要并发必须用 gather、create_task 或 TaskGroup。

    四、实战案例

    案例1:异步并发请求(最经典场景)

    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://api1.com", "https://api2.com", "https://api3.com"]
    async with aiohttp.ClientSession() as session:
    tasks = [fetch_url(session, url) for url in urls]
    results = await asyncio.gather(*tasks)
    for url, html in zip(urls, results):
    print(f"{url}: {len(html)} 字符")

    asyncio.run(main())

    同步方式总耗时 ≈ 所有请求耗时之和;异步方式总耗时 ≈ 最慢那个请求的耗时。

    案例2:Future 手动控制

    import asyncio

    async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()

    # 模拟:2秒后设置结果
    loop.call_later(2, lambda: fut.set_result("完成"))

    # 同步任务转异步
    def blocking():
    import time
    time.sleep(1)
    return "同步任务结果"

    fut2 = loop.run_in_executor(None, blocking) # 返回 Future
    result = await fut2
    print(result)

    run_in_executor 可以将阻塞的同步函数放到线程池执行,返回 Future 对象。

    案例3:超时控制

    async def main():
    try:
    result = await asyncio.wait_for(my_coroutine(), timeout=5.0)
    except asyncio.TimeoutError:
    print("操作超时!")

    五、高频面试考点

    考点1:async def 和普通 def 的区别?

    • async def 定义协程函数,调用返回协程对象,不会立即执行
    • 普通 def 直接调用即执行
    • 协程对象必须通过事件循环或 await 执行

    考点2:await 到底做了什么?

    • 挂起当前协程,保存执行上下文(局部变量、指令指针)
    • 将控制权交还给事件循环
    • 当被等待对象完成后,事件循环恢复协程执行
    • 不是简单的“等待” ,而是“注册回调 + 暂停自己”

    考点3:事件循环是什么?怎么工作的?

    • 是 asyncio 的调度核心,本质是一个 while 循环
    • 维护就绪队列和等待队列
    • 从就绪队列取任务执行,遇到 await 就挂起并放入等待队列
    • I/O 就绪/定时器触发后唤醒对应协程
    • 基于操作系统的 I/O 多路复用机制(epoll/kqueue/IOCP)

    考点4:Future 和 Task 的区别?

    Future

    Task

    本质

    异步操作结果的占位符

    Future 的子类

    职责

    只表示结果,不驱动操作

    封装协程并驱动执行

    创建

    loop.create_future()

    asyncio.create_task()

    考点5:asyncio.sleep(1) 返回的是什么?是协程还是 Future?

    返回的是协程对象。当 await asyncio.sleep(1) 时,这个协程内部实际上创建了一个 Future,并把自身挂起在这个 Future 上。定时器到期后,回调触发 Future.set_result(None),进而恢复外层协程。

    考点6:gather 和 wait 的区别?

    • gather:返回结果列表,异常立即传播
    • wait:返回 (done, pending),支持精细控制

    考点7:为什么说协程能高效处理 I/O 密集型任务?

    因为协程在等待 I/O 时主动让出控制权,事件循环可以调度其他协程执行。单线程内实现了并发(不是并行),没有线程切换开销。

    六、面试回答思路

    面试时被问“协程怎么工作的”,不要只背语法,按 “问题—机制—对比” 结构展开:

  • 先说场景:“比如发10个HTTP请求,同步要串行等待约10秒;用协程,发起第一个请求后立刻 await,控制权交还事件循环,接着发第二个……10个请求几乎同时发出。”
  • 再拆关键动作:“await 不是‘等结果’,而是‘注册回调 + 暂停自己’;事件循环用 epoll 监听 socket 状态,一就绪就唤醒对应协程。”
  • 最后点明边界:“协程提升的是 I/O 并发效率,不是 CPU 计算速度;CPU 密集任务要用多进程或 run_in_executor。”
  • 七、总结

    async def 定义协程(调用不执行),await 挂起协程让出控制权,事件循环负责调度,Future 是结果占位符,Task 是 Future 的子类用于封装协程。整个体系在单线程内通过协作式调度实现 I/O 密集型任务的高并发。

    八、学习链接

    https://www.bilibili.com/video/BV1JsLDzAEGu?spm_id_from=333.788.player.switch&vd_source=565c1342b6f8b08590419749e9babc0a&p=6

    赞(0)
    未经允许不得转载:171主机测评 » 【学习笔记】Python异步编程:如何让程序“一心多用”还不乱套?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址