Python进阶教程丨深入理解协程

1. 什么是协程?(Coroutine)

1.1. 协程 vs 线程 vs 进程

我们之前讲过线程和进程,今天要引入一个新的概念:协程。

在计算机科学中,协程(Coroutine)、线程和进程是三种不同的并发执行模型,它们的核心差异在于资源占用调度方式

维度

进程

线程

协程

资源隔离

独立内存空间

共享进程内存

共享线程内存

切换成本

高(需切换内存空间)

中(需操作系统调度)

极低(用户代码控制)

并发数量

数百

数千

数万

数据共享

进程间通信(IPC)

直接共享

直接共享

适用场景

CPU密集型任务

I/O密集型任务

高并发I/O密集型任务

1.2. 为什么需要协程

假设我们要为某雪冰城开发订餐系统,当遇到200个用户同时点单的情况时,我们可以进行下面的分配:

  • 进程场景:开200 家独立营业的门店,每家制作一位顾客的奶茶。(每个用户独占完整资源)
  • 线程场景:一家门店内调度200位饮品师(共享资源但消耗大量人力管理成本)
  • 协程场景:一位店员说明饮品名称,一位店员添加小料,一位加水加冰,一位去封口……每人各司其职,相互协作,200杯奶茶如流水线般丝滑流转。

这就是协程的价值所在——以较小的资源消耗实现高效的并发处理。在Python中每个协程只需要5KB左右的内存空间,比线程(约8MB)轻量1600倍,可以实现数万级的并发。

1.3. 协程的核心:协作式多任务(非抢占式)

协作式:协程 主动让出执行权(通过 awaityield),其他协程才能运行。
非抢占式:操作系统不会强制暂停协程,完全由开发者控制切换时机。

优势

  • 避免锁竞争:无需线程锁,减少死锁风险。
  • 降低切换开销:协程切换在用户态完成,无需内核介入(速度比线程快10-100倍)。

2. 协程的具体实现方法

2.1. asyncawait 关键字(Python 3.5+)

如果你之前阅读过网络编程那篇文章,或许对这个关键字有一些印象。

async def:定义协程函数,调用时返回协程对象(不立即执行)。
await:挂起当前协程,等待异步操作完成(如I/O、其他协程)。

async def fetch_user_data(user_id):
    print(f"Fetching data for user {user_id}...")
    await asyncio.sleep(1)  # 模拟网络请求
    return {"id": user_id, "name": "Alice"}

# 调用协程(需通过事件循环)
result = asyncio.run(fetch_user_data(1))

2.2. 事件循环(Event Loop)

核心作用:事件循环是协程的调度器,管理所有异步任务的执行。
工作原理

  1. 维护一个 任务队列(Task Queue)
  2. 循环从队列中取出任务执行,遇到 await 挂起当前任务,执行下一个。
  3. 当异步操作(如I/O)完成时,将结果返回给对应协程。

通俗比喻
事件循环像一个餐厅服务员,同时服务多桌顾客。每当一桌顾客需要等待上菜(await),服务员就去服务其他桌,等菜好了再回来继续。

示例:简单异步任务调度

import asyncio

async def boil_water():
    print("Start boiling water")
    await asyncio.sleep(3)  # 模拟耗时3秒
    print("Water boiled!")

async def chop_vegetables():
    print("Start chopping vegetables")
    await asyncio.sleep(2)  # 模拟耗时2秒
    print("Vegetables chopped!")

async def cook_dinner():
    await asyncio.gather(boil_water(), chop_vegetables())

asyncio.run(cook_dinner())

# 输出:
# Start boiling water
# Start chopping vegetables
# Vegetables chopped! (2秒后)
# Water boiled! (3秒后)

关键点asyncio.gather() 并发执行多个协程,总耗时由最长任务决定(3秒)。


2.3. 协程的进阶用法

2.3.1. 使用 asyncio 库管理协程

核心API
asyncio.create_task():将协程包装为任务,加入事件循环。
asyncio.sleep():非阻塞式休眠,模拟I/O等待。
asyncio.run():启动事件循环的入口函数。

async def main():
    task1 = asyncio.create_task(boil_water())
    task2 = asyncio.create_task(chop_vegetables())
    await task1
    await task2

2.3.2. 协程间通信:asyncio.Queue

实现生产者-消费者模型:

async def producer(queue):
    for i in range(3):
        await queue.put(i)
        print(f"Produced {i}")
        await asyncio.sleep(0.5)

async def consumer(queue):
    while True:
        item = await queue.get()
        print(f"Consumed {item}")
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    await asyncio.gather(producer(queue), consumer(queue))

2.3.3. 协程的异常处理

捕获异常:使用 try/except 包裹 await
取消任务:调用 task.cancel() 并处理 asyncio.CancelledError

async def risky_task():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        print("Task was cancelled!")
        raise

async def main():
    task = asyncio.create_task(risky_task())
    await asyncio.sleep(1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("Main caught cancellation")

3. 协程的典型应用场景

3.1. 高并发网络请求

案例:异步爬虫并发下载1000个页面。

import aiohttp

async def download_page(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    urls = ["https://example.com" for _ in range(1000)]
    tasks = [download_page(url) for url in urls]
    pages = await asyncio.gather(*tasks)

3.2. I/O密集型任务

案例:异步批量写入文件。

import aiofiles

async def write_file(filename, data):
    async with aiofiles.open(filename, 'w') as f:
        await f.write(data)

async def batch_write():
    tasks = []
    for i in range(100):
        tasks.append(write_file(f"file_{i}.txt", "data"))
    await asyncio.gather(*tasks)

3.3. 实时数据处理

案例:聊天服务器处理多用户消息。

async def handle_client(reader, writer):
    while True:
        data = await reader.read(1024)
        if not data:
            break
        message = data.decode()
        writer.write(f"Echo: {message}".encode())
        await writer.drain()
    writer.close()

async def run_server():
    server = await asyncio.start_server(handle_client, '0.0.0.0', 8888)
    async with server:
        await server.serve_forever()

协程通过 协作式多任务事件循环 机制,在单线程内实现高并发处理,尤其适合网络请求、文件I/O等场景。相比多线程,协程的 资源占用更低开发效率更高,已成为现代Python高并发编程的核心技术。


4. 生成器与协程

4.1. 关系与区别

生成器与协程在Python中是紧密相关的概念,二者均可以实现暂停与恢复,但设计目标不同:

1. 生成器是协程的基础

生成器最初仅用于惰性生成数据(如yield产生序列),但通过.send()方法扩展后,可实现简单的双向通信,成为协程的雏形。

2. 协程是生成器的进化

  • Python 3.5引入async/await语法,明确区分协程与生成器:
  • 生成器:用def定义,含yield,专注于数据生产(如逐行读取文件)。
  • 协程:用async def定义,含await,专注于任务协作(如并发处理网络请求)。

示例

# 生成器:单向生产数据
def count_up(n):
    i = 0
    while i < n:
        yield i  # 只能向外发送数据
        i += 1

# 生成器改协程:双向通信
def echo():
    while True:
        received = yield  # 可接收外部数据
        print(f"Echo: {received}")

# 现代协程:异步协作
async def fetch_data(url):
    response = await aiohttp.get(url)  # 明确异步等待
    return response.json()

生成器是协程的“前身”,协程通过语法和功能扩展,成为异步编程的核心工具,我们用表格总结一下:

维度

生成器(Generator)

协程(Coroutine)

设计目标

惰性生成数据(生产者模型)

协作式处理任务(消费者模型)

通信方向

单向(生成器 → 调用方)

双向(可接收外部输入并返回结果)

调度方式

next()for 循环驱动

由事件循环(Event Loop)调度

核心语法

yield(Python 2.5+)

async/await(Python 3.5+)

典型场景

数据流生成、内存敏感的大数据处理

高并发I/O操作、异步任务编排

4.2. 不同场景如何选择?

1️⃣ 适合选择生成器的场景:
• 需要按需生成大规模数据(如日志文件、传感器数据流)。
• 内存敏感,避免一次性加载全部数据。
• 无需复杂任务调度,只需简单数据生产。
• 生成无限序列(如斐波那契数列、素数序列)。
• 数据清洗与转换的流水线(Pipeline)。

2️⃣ 适合选择协程的场景:
• 高并发I/O操作(如网络请求、数据库查询)。
• 需要异步任务编排,协调多个依赖操作。
• 实时响应与低延迟(如聊天服务器、游戏服务器)。
• Web服务器处理数千并发连接。
• 爬虫并发下载页面。

概念

生成器

协程

核心思想

数据驱动:按需生成,节省内存

任务驱动:协作调度,提升吞吐量

适用层级

单任务数据生产

多任务协作与并发

资源消耗

极低(仅保存当前状态)

低(单线程内百万级并发)

我们可以类比的来理解双方的概念:
生成器像一条流水线,持续生产产品(数据),但不会主动协调其他工序。
协程像一群 协作的工人,每个人完成自己的工作后主动通知他人,共同完成任务。

通过理解两者的设计目标与适用场景,我们可以更精准地选择工具