代码改变世界

Python并发编程学习 day3

2025-06-30 15:12  第二个卿老师  阅读(45)  评论(0)    收藏  举报

Day 3:Python 协程编程基础

一、核心概念

  • 异步IO(Async IO):一种编程范式(模型),程序可能不会等待一个执行步骤完成就转到下一个执行步骤

异步IO是一种并发编程风格,但不是并行,它是一种单进程、单线程的设计,使用了协作式多任务处理
异步 IO 需要较长的等待时间,否则函数会被阻塞,并允许其他函数在停机时间内运行

  • 协程(Coroutine)

    • 本质上是可以在执行过程中暂停并切换的函数
    • 用于构建非阻塞 I/O 编程模型(单线程高并发)
  • 事件循环(Event Loop)

    • 负责调度和执行所有异步任务,由 asyncio 提供
  • 关键字语法

    • async def 定义协程函数
    • await 挂起等待另一个协程完成

    示例:

    import asyncio
    
    async def hello():
        print("Hello")
        await asyncio.sleep(1)
        print("World")
    
    asyncio.run(hello())
    

协程属于可等待对象,另外task与future也属于可等待对象,task是协程高层级的封装,而future是低层级可等待对象

二、asyncio 核心 API(Python 3.10)

1. 启动入口

asyncio.run(main())   # 通常用来运行最高层级的入口点 "main()" 函数

2. 创建任务:

  • asyncio.create_task():将协程包装为任务并立即调度执行
async def worker(name):
    print(f"Start {name}")
    await asyncio.sleep(1)
    print(f"End {name}")

async def main():
    task1 = asyncio.create_task(worker("A"))
    task2 = asyncio.create_task(worker("B"))
    await task1
    await task2

主要用于控制单个任务,任务会立即启动并且需要单独await task来获取结果,3.11版本及以后推荐使用asyncio.TaskGroup.create_task() 来代替

3. 并发调度任务

  • asyncio.gather(*tasks,return_exceptions=False):自动把协程作为一个任务调度,返回所有可等待对象都成功完成的返回值列表
async def factorial(name, number):
    f = 1
    for i in range(2, number + 1):
        print(f"Task {name}: Compute factorial({number}), currently i={i}...")
        await asyncio.sleep(1)
        f *= i
    print(f"Task {name}: factorial({number}) = {f}")
    return f

async def main():
    # 将三个调用 *并发地* 加入计划任务:
    L = await asyncio.gather(
        factorial("A", 2),
        factorial("B", 3),
        factorial("C", 4),
    )
    print(L)

asyncio.run(main()) 

主要用于批量管理任务组,启动任务并等待所有完成,自动获取所有任务结果。默认情况下,一个任务失败会导致整体失败(除非 return_exceptions=True)

  • asyncio.wait(*tasks, *, timeout=None, return_when=ALL_COMPLETED):监控一组任务的完成状态,可设置完成条件(如:FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED)
async def task(n):
    await asyncio.sleep(n)
    return f"Task-{n}"

async def main():
    tasks = [asyncio.create_task(task(i)) for i in [3, 1, 2]]
    
    # 等待第一个任务完成
    done, pending = await asyncio.wait(
        tasks,
        return_when=asyncio.FIRST_COMPLETED
    )
    
    print(f"第一个完成: {next(iter(done)).result()}")
    print(f"仍在运行: {len(pending)}")

asyncio.run(main())

asyncio.wait()返回已完成和未完成的任务集合,可设置超时但不自动取消任务

4. 协程与超时控制

  • asyncio.wait_for(task, timeout):为单个异步操作添加超时控制,防止无限期阻塞
async def long_operation():
    await asyncio.sleep(5)
    return "结果"

async def main():
    try:
        # 设置3秒超时
        result = await asyncio.wait_for(long_operation(), timeout=3)
    except asyncio.TimeoutError:
        print("操作超时!")
    else:
        print(f"结果: {result}")

asyncio.run(main())

asyncio.wait_for()与asyncio.wait()不同,前者专门处理单个任务且超时自动取消任务

5. 异步上下文管理器 & 异步迭代器

  • async with
    语法:async with expr as var,主要用于管理异步资源的获取/释放,进入/退出代码块时挂起,常用场景包括网络连接、数据库连接、文件操作
  • async for
    语法:async for item in async_iterable,主要用于异步遍历异步可迭代对象,每次迭代时挂起,常用场景包括流式数据处理、实时消息处理、分页API

三、协程适用场景

场景 推荐方案 原因
I/O密集型任务 asyncio 协程 单线程高并发、低开销
网络请求 / 爬虫 aiohttp + asyncio 高并发非阻塞请求
高频文件操作 aiofiles 异步 I/O 释放控制权

四、协程编程注意事项

  • 阻塞调用禁止:不要在协程中使用 time.sleep()requests.get() 等阻塞操作
  • 使用 aio 版本库:如 aiohttpaiomysqlaiofiles
  • 异常处理机制:注意 await 表达式的异常抛出 & 捕获方式

五、练习任务:高并发异步请求模拟

目标:使用 asyncio + aiohttp 模拟发起多个异步 HTTP 请求并收集结果

import asyncio
import aiohttp

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

async def main():
    urls = ["https://httpbin.org/delay/1" for _ in range(10)]
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(fetch(session, url)) for url in urls]
        results = await asyncio.gather(*tasks)
        print(f"共返回 {len(results)} 条数据")

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

六、今日思考题

  1. 为什么协程可以提升 I/O 并发性能,但不能加速 CPU 计算?
    答:因为协程的原理是在等待IO时,CPU存在空闲状态,就可以运行其他任务,而CPU计算会一直占有CPU,就没有空闲状态
  2. create_taskgather 的区别是什么?
    答:create_task操作单个任务而gather操作多个任务,前者运行时立即启动任务,不阻塞当前流程,但需要手动等待获取结果,后者运行时启动任务并等待所有任务完成,自动返回所有结果的有序列表
  3. async with 和普通 with 有什么本质差别?
    答:从两者的执行模型和适用场景来说:普通 with 是同步阻塞的,适用于快速完成的操作,async with 是异步非阻塞的,适用于I/O密集型操作,async with 通过挂起协程而非阻塞线程来实现高效并发