python快速入门学习笔记(进阶篇)四十:协程 Coroutine 进阶 asyncio 与 IO 密集型场景深度对比(上)

  • 原创
  • 作者:程序员三丰
  • 发布时间:2026-06-13 20:53
  • 浏览量:5
Python入门第四十课,主要针对asyncio的核心价值IO密集型任务的处理上,从底层维度拆解同步与异步的差异,为深度理解 asyncio 核心概念奠定基础。

一、同步VS异步 深度对比(底层逻辑+性能本质)

asyncio的核心价值体现在IO密集型任务的处理上,先从底层维度拆解同步与异步的本质差异,为理解asyncio奠定基础。

1、核心维度深度对比表:

对比维度 同步编程(Sync) 异步编程(Async,基于 asyncio)
执行模型 串行阻塞:代码按照顺序执行,遇到IO(网络/文件/数据库)时,整个进程/线程暂停等待,直到IO完成 非阻塞并发:代码执行到IO时,协程挂起(放弃CPU),事件循环调度其他就绪协程执行,IO完成后恢复原协程。
底层调度 操作系统内核调度(线程/进程切换),切换成本高(内核态→用户态) 用户态调度(事件循环调度协程),切换成本极低(仅函数调用级)
资源占用 为避免阻塞,需创建多线程/多进程(如1000个请求需要1000个线程),内存占用高(每个线程栈≈1MB) 单线程/少量线程即可处理大量并发(1000个请求仅需1个线程),内存占用极低
IO处理逻辑 主动等待(Polling):CPU空转等待IO结果 被动通知(Event-driven):IO完成后由操作系统触发回调
性能表现 IO密集型任务:总耗时≈各任务耗时之和(如5个2秒任务→10秒);
CPU密集型:无劣势
IO密集型任务:总耗时≈最长单个任务耗时(如5个2秒任务→2秒);
CPU密集型:无优势(协程无法利用多核)
编程思维 线性编程:代码执行顺序=逻辑顺序,易理解 异步思维:需关注协程挂起/恢复点,需结合await/事件循环,入门门槛高
典型实现 requests(HTTP)、pymysql(数据库)、time.sleep() aiohttp(HTTP)、aiomysql(数据库)、asyncio.sleep()

2、直观性能对比:同步VS异步处理IO密集型任务

以下代码模拟「批量网络请求(IO密集型)」场景,清晰体现异步的性能优越性:

  • 同步版本(耗时高)
"""
sync 同步实现【模拟】
"""

# 客户端发起请求 → 服务端接收请求 → 服务端处理请求
from time import time, sleep


# 服务端处理请求(响应)
def web_request_response(*args, **kwargs):
    print('=== 服务端响应 ===')
    print(f'[args]: {args}')
    print(f'[kwargs]: {kwargs}')
    sleep(3)


# 发起请求,接收服务端响应
def web_request():
    web_request_response()


# 模拟客户端发送请求
def client():
    begin_time = time()
    for x in range(1, 4):
        if x > 1: print()
        print(f'=== 客户端第{x}次请求 ===')
        web_request()
    spent_time = time() - begin_time
    print(f'\n[同步请求耗时]: {spent_time}秒')


if __name__ == '__main__':
    client()

上面的代码运行结果是,同步请求耗时:9.001716136932373秒

  • 异步版本(耗时低)
"""
async 异步实现【模拟】
"""

# 客户端发起请求 → 服务端接收请求 → 服务端处理请求

import asyncio
from time import time


# 服务端处理请求(响应)
async def web_request_response(*args, **kwargs):
    print('=== 服务端响应 ===')
    print(f'[args]: {args}')
    print(f'[kwargs]: {kwargs}')
    await asyncio.sleep(3)


# 发起请求,接收服务端响应
async def web_request():
    await web_request_response()


# 模拟客户端发送请求
async def client():
    begin_time = time()
    tasks = []
    for x in range(1, 4):
        if x > 1: print()
        print(f'=== 客户端第{x}次请求 ===')
        tasks.append(asyncio.create_task(web_request()))
    for task in tasks:
        await task
    spent_time = time() - begin_time
    print(f'\n[异步请求耗时]: {spent_time}秒')


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

上面的代码运行结果是,异步请求耗时:3.0060555934906006秒

3、核心差异总结(异步优越性)

  • 耗时差异:同步版本总耗时=5×2=10秒(串行等待),异步版本总耗时≈2秒(所有任务并发执行,仅等待最长的IO耗时);
  • CPU利用率:同步版本CPU空转90%以上(等待IO),异步版本CPU在IO等待期间可调度其他任务,利用率接近100%;
  • 资源占用:同步版本若处理1000个请求需创建1000个线程(内存占用≈1GB),异步版本仅需1个线程(内存占用≈MB级别)。

二、asyncio 核心概念(深度理解)

要掌握asyncio,需先吃透以下核心概念,这是理解异步编程的基础:

1、协程(Coroutine):异步编程的基本单元

  • 定义

    通过async def定义的函数,调用后返回『协程对象』(而非直接执行),需通过事件循环调度执行。

  • 核心特性

    可暂停(await)、可恢复,暂停时不阻塞线程,是用户态的“轻量级线程”。

  • 与线程的区别

    线程:内核调度,切换成本高(需保存寄存器/栈帧);

    协程:用户态调度(事件循环),切换成本仅为函数调用,无内核开销。

2、事件循环(Event Loop):asyncio的“心脏”

  • 定义

    asyncio的核心调度器,负责管理所有协程的生命周期。一个事件循环每次运行一个 Task 对象。

  • 核心职责

    ➊ 注册/调度协程/Task;

    ➋ 监听IO事件(网络/文件),触发回调;

    ➌ 切换挂起的协程,实现非阻塞执行;

    ➍ 处理定时器、信号等异步任务。

  • 底层实现

    基于操作系统的IO多路复用(epoll/kqueue/select),实现“单线程监听多IO事件”。

3、可等待对象(Awaitable):await 的唯一合法操作数

  • await右侧必须是『可等待对象』,包含三类:

    ➊ 协程对象async def函数调用的返回值;

    ➋ Task 对象:协程的可调度封装(asyncio.create_task()创建),事件循环的直接调度单元;

    ➌ Future 对象:表示“未来完成的异步操作结果”,Task 继承自 Future,底层用于封装异步IO的结果。

4、Task vs Future

  • 概念介绍
    Future:底层抽象,手动创建需调用set_result()/set_exception()标记完成,通常无需手动使用;

    Task:Future 的子类,专为协程设计,create_task()自动将协程封装为Task,并交由事件循环调度,是实际开发中最常用的对象。

5、事件循环策略(Event Loop Policy)

  • 定义

    管理事件循环的创建/获取/销毁的规则,默认由asyncio提供;

  • 进阶优化

    Linux 下可替换为uvloop(基于libuv实现,性能比默认循环快2-4倍)。

    安装:

      pip install uvloop
    

    使用:

      import asyncio
      import uvloop
    
      # 设置uvloop为默认循环事件
      asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    

三、asyncio 基础语法(核心入门)

1、协程的定义与执行(基础)

  • 定义协程函数(async def是标志)
import asyncio

# 定义协程函数
async def worker(num: int) -> int:
    print('协程对象开始执行')
    await asyncio.sleep(num)
    print('协程对象结束执行')
    return num
  • 执行协程的三种方式:

➊ 方式1:asyncio.run()运行一个顶层协程,管理事件循环的生命周期,是程序的主入口。Python3.7+推荐,适用于单协程执行、脚本入口。

import asyncio
from time import time


# 定义协程函数
async def worker(num: int) -> int:
    print('协程对象开始执行')
    await asyncio.sleep(num)
    print('协程对象结束执行')
    return num


# 协程执行的第一种方式:往往被用作协程脚本的入口
def run_function_1():
    # asyncio.run(需要传入协程对象),每执行一次run方法就会创建一个事件循环
    # 所以下面的代码会创建3个时间循环
    begin = time()
    res1 = asyncio.run(worker(3))
    res2 = asyncio.run(worker(3))
    res3 = asyncio.run(worker(3))
    spent = time() - begin
    print(f'[第一种方式]:{res1} {res2} {res3},耗时:{spent}秒')


if __name__ == '__main__':
    run_function_1()

上面的代码运行结果:

协程对象开始执行
协程对象结束执行
协程对象开始执行
协程对象结束执行
协程对象开始执行
协程对象结束执行
[第一种方式]:3 3 3,耗时:9.018600463867188秒

执行结果是9秒,为什么不是我们期望的3秒呢?原因是:asyncio.run()每运行一次都会创建一个新的事件循环,那么上面的代码就创建了3个事件循环,所以执行结果和同步是一样的效果。

➋ 方式2:手动管理事件循环(兼容Python3.6以下),适用于旧版本兼容、自定义事件循环配置。

由于老版本已经基本很少使用了,了解下面的语法即可:(注意:下面的代码只是展示语法,并未实际运行)

"""协程执行旧版本的写法 Python3.6以下"""

import asyncio
from time import time


async def worker(num: int) -> int:
    await asyncio.sleep(num)
    return num


def run_function_2():
    begin_time = time()
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(worker(5))
        loop.run_until_complete(worker(5))
        loop.run_until_complete(worker(5))
    except Exception as e:
        raise e
    spent_time = time() - begin_time
    print('耗时:{spent_time}' . format(spent_time))


if __name__ == '__main__':
    run_function_2()

➌ 方式3:create_task + asyncio.run并发执行多协程,使用于生产环境(体现asyncio核心价值)。asyncio.create_task()将协程包装成一个 Task 对象,并排入事件循环等待调度。这是实现并发的主要方式。

import asyncio
from time import time


# 定义协程函数
async def worker(num: int) -> int:
    print('协程对象开始执行')
    await asyncio.sleep(num)
    print('协程对象结束执行')
    return num


# 协程执行的第三种方式:create_task()方式
async def run_function_3():
    begin = time()
    # 下面的写法,是将三个协程Task对象,封装到一个事件循环之中
    task1 = asyncio.create_task(worker(1))
    task2 = asyncio.create_task(worker(2))
    task3 = asyncio.create_task(worker(3))
    res1 = await task1
    res2 = await task2
    res3 = await task3
    spent = time() - begin
    print(f'[第三种方式]:{res1} {res2} {res3},耗时:{spent}秒')


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

上面的代码运行结果:

协程对象开始执行
协程对象开始执行
协程对象开始执行
协程对象结束执行
协程对象结束执行
协程对象结束执行
[第三种方式]:1 2 3,耗时:3.0070319175720215秒

这次运行结果,就是我们期望看到的结果了。

总结:一般我们是把第一种和第三种结合使用,把 asyncio.run()作为协程脚本的入口,将 asyncio.create_task() 作为封装过程。

2、async/await 核心规则(重要)

  • async def定义的函数只能返回『协程对象』,不能直接执行;
  • await仅能在async def函数内使用,右侧必须是可『等待对象』;
  • 未被await的协程对象不会执行(仅创建,无调度);
  • 嵌套协程:async def函数内可调用其他async def函数,但必须加await

3、错误示例与修正(避坑)

import asyncio

# 错误1:普通函数使用await
def wrong_func():
    # await asyncio.sleep(1) # SyntaxError: 'await' outside async function
    pass

# 错误2:await 非可等待对象
async def wrong_await():
    await 123


# 错误3:未await协程对象(仅创建,不执行)
async def worker():
    await asyncio.sleep(1)

async def no_await_coro():
    coro = worker() # 仅创建协程对象,无调度
    await asyncio.sleep(1) # 上面的协程对象未执行

下篇文章,将继续分享 asyncio 的核心API,异步上下文管理器,异步锁和异步信号量。

声明:本文为原创文章,51blog.xyz和作者拥有版权,如需转载,请注明来源于51blog.xyz并保留原文链接:https://www.51blog.xyz/article/142.html

文章归档

推荐文章

buildadmin logo
Thinkphp8 Vue3 Element PLus TypeScript Vite Pinia

🔥BuildAdmin是一个永久免费开源,无需授权即可商业使用,且使用了流行技术栈快速创建商业级后台管理系统。

热门标签

PHP ThinkPHP ThinkPHP5.1 Go Mysql Mysql5.7 Redis Linux CentOS7 Git HTML CSS CSS3 Javascript JQuery Vue LayUI VMware Uniapp 微信小程序 docker wiki Confluence7 学习笔记 uView ES6 Ant Design Pro of Vue React ThinkPHP6.0 chrome 扩展 翻译工具 Nuxt SSR 服务端渲染 scrollreveal.js ThinkPHP8.0 Mac webman 跨域CORS vscode GitHub ECharts Canvas vue3 three.js 微信支付 PHP全栈开发 Python AI 人工智能 AI生成 工作经验 实战笔记