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. 协程的核心:协作式多任务(非抢占式)
协作式:协程 主动让出执行权(通过 await
或 yield
),其他协程才能运行。
非抢占式:操作系统不会强制暂停协程,完全由开发者控制切换时机。
优势:
- 避免锁竞争:无需线程锁,减少死锁风险。
- 降低切换开销:协程切换在用户态完成,无需内核介入(速度比线程快10-100倍)。
2. 协程的具体实现方法
2.1. async
和 await
关键字(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)
• 核心作用:事件循环是协程的调度器,管理所有异步任务的执行。
• 工作原理:
- 维护一个 任务队列(Task Queue)。
- 循环从队列中取出任务执行,遇到
await
挂起当前任务,执行下一个。 - 当异步操作(如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) |
设计目标 |
惰性生成数据(生产者模型) |
协作式处理任务(消费者模型) |
通信方向 |
单向(生成器 → 调用方) |
双向(可接收外部输入并返回结果) |
调度方式 |
由 |
由事件循环(Event Loop)调度 |
核心语法 |
|
|
典型场景 |
数据流生成、内存敏感的大数据处理 |
高并发I/O操作、异步任务编排 |
4.2. 不同场景如何选择?
1️⃣ 适合选择生成器的场景:
• 需要按需生成大规模数据(如日志文件、传感器数据流)。
• 内存敏感,避免一次性加载全部数据。
• 无需复杂任务调度,只需简单数据生产。
• 生成无限序列(如斐波那契数列、素数序列)。
• 数据清洗与转换的流水线(Pipeline)。
2️⃣ 适合选择协程的场景:
• 高并发I/O操作(如网络请求、数据库查询)。
• 需要异步任务编排,协调多个依赖操作。
• 实时响应与低延迟(如聊天服务器、游戏服务器)。
• Web服务器处理数千并发连接。
• 爬虫并发下载页面。
概念 |
生成器 |
协程 |
核心思想 |
数据驱动:按需生成,节省内存 |
任务驱动:协作调度,提升吞吐量 |
适用层级 |
单任务数据生产 |
多任务协作与并发 |
资源消耗 |
极低(仅保存当前状态) |
低(单线程内百万级并发) |
我们可以类比的来理解双方的概念:
• 生成器像一条流水线,持续生产产品(数据),但不会主动协调其他工序。
• 协程像一群 协作的工人,每个人完成自己的工作后主动通知他人,共同完成任务。
通过理解两者的设计目标与适用场景,我们可以更精准地选择工具