Python案例中的协程如何使用?从基础到实战,一文掌握异步编程
目录导读
- 什么是协程?为什么它比线程更适合IO密集型任务?
- 协程的核心概念:
async、await与事件循环 - 实战案例一:使用
asyncio实现并发网络请求 - 实战案例二:协程与
aiohttp构建高效爬虫 - 实战案例三:协程在文件读写与数据库操作中的应用
- 常见问题与避坑指南(含问答)
- 何时该用协程,何时该用线程?
什么是协程?为什么它比线程更适合IO密集型任务?
协程(Coroutine) 是Python中一种轻量级的并发编程方式,它允许函数在执行过程中暂停,并在未来某个时刻恢复执行,与线程不同,协程的切换完全由程序控制(而非操作系统),因此开销极低——一个进程中可以轻松创建数十万个协程,而线程通常只能支持几千个。

为什么协程在IO密集型任务中表现优异?
因为当程序等待网络响应、磁盘读取或数据库查询时,协程可以主动让出CPU,去执行其他协程,而线程在IO等待时会被操作系统挂起,造成上下文切换开销,举个例子:如果你需要同时下载100个网页,使用线程会导致频繁切换,而协程只需一个线程即可高效完成。
问答环节
Q:协程能替代线程吗?
A:不能完全替代,对于CPU密集型任务(如视频编码、数学计算),多线程或多进程更合适,协程擅长IO密集型任务,如网络请求、文件读写、数据库操作。
协程的核心概念:async、await与事件循环
Python从3.5起通过async/await语法原生支持协程,理解以下三个概念是关键:
async def:定义一个协程函数,调用它返回一个协程对象,而非立即执行。await:挂起当前协程,等待另一个协程或Future对象完成,仅能在async def内使用。- 事件循环(Event Loop):调度并执行协程的核心引擎。
asyncio.run()是启动事件循环的最简方式。
基础示例:
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1) # 模拟IO等待,让出CPU
print("World")
asyncio.run(say_hello()) # 输出:Hello(1秒后)World
问答环节
Q:asyncio.sleep(1)与time.sleep(1)有什么区别?
A:time.sleep会阻塞整个线程,导致其他协程无法执行;而asyncio.sleep会挂起当前协程,让事件循环调度其他任务。
实战案例一:使用asyncio实现并发网络请求
假设我们需要从3个API获取数据,传统同步方式需要3秒(每请求1秒),使用协程可将其缩短至1秒。
代码实现:
import asyncio
import time
async def fetch_data(url, delay):
await asyncio.sleep(delay) # 模拟网络延迟
return f"Data from {url}"
async def main():
tasks = [
fetch_data("https://api.example.com/1", 1),
fetch_data("https://api.example.com/2", 1),
fetch_data("https://api.example.com/3", 1)
]
results = await asyncio.gather(*tasks) # 并发执行所有协程
print(results) # 1秒后输出
start = time.time()
asyncio.run(main())
print(f"耗时:{time.time() - start:.2f}秒") # 约1秒
关键点:
asyncio.gather()将多个协程打包并发执行,并收集结果。- 如果其中一个协程抛出异常,其他协程仍会继续(除非设置
return_exceptions=True)。
问答环节
Q:如果任务数量达到1000个,asyncio.gather会一次性创建所有任务吗?
A:是的,可通过asyncio.Semaphore限制并发数量,例如一次只允许10个请求并发。
实战案例二:协程与aiohttp构建高效爬虫
纯asyncio无法直接发送HTTP请求,需要搭配第三方库aiohttp,这是一个异步HTTP客户端。
安装: pip install aiohttp
示例:并发爬取5个网页
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
print(f"Got {url} with status {response.status}")
return await response.text()
async def main():
urls = [
"https://www.example.com",
"https://httpbin.org/get",
"https://api.github.com",
"https://jsonplaceholder.typicode.com/posts/1",
"https://httpstat.us/200"
]
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
pages = await asyncio.gather(*tasks)
print(f"爬取完成,共{len(pages)}个页面")
asyncio.run(main())
性能对比: 同步爬取5个网页约5秒,协程版本可在1秒内完成(受网络延迟影响)。
问答环节
Q:aiohttp与requests库能混用吗?
A:不建议。requests是同步阻塞库,在协程中使用会阻塞事件循环,导致并发失效,必须使用异步库(如aiohttp、httpx)。
实战案例三:协程在文件读写与数据库操作中的应用
除了网络请求,协程也适用于文件系统操作和数据库查询,Python的aiofiles库提供异步文件读写,asyncpg或aiomysql提供异步数据库驱动。
异步文件写入示例(需安装aiofiles):
import asyncio
import aiofiles
async def write_large_file():
async with aiofiles.open("large.txt", "w") as f:
for i in range(10000):
await f.write(f"Line {i}\n") # 非阻塞写入
print("文件写入完成")
asyncio.run(write_large_file())
异步数据库查询(使用asyncpg示例):
import asyncio
import asyncpg
async def query_db():
conn = await asyncpg.connect(user='user', password='pass',
database='test', host='127.0.0.1')
rows = await conn.fetch('SELECT * FROM users')
await conn.close()
return rows
result = asyncio.run(query_db())
问答环节
Q:为什么文件的write操作也需要异步?
A:普通文件写入在数据量大时仍然会阻塞线程,异步写入允许在等待磁盘操作时,事件循环去处理其他协程。
常见问题与避坑指南(含问答)
问题1:协程中的异常处理
协程中的异常会向上传播,若未被捕获,可能导致事件循环停止。
正确做法:
async def risky_task():
try:
# 可能出错的代码
await asyncio.sleep(1)
raise ValueError("出错")
except Exception as e:
print(f"异常已捕获:{e}")
asyncio.run(risky_task())
问题2:不要在协程中调用同步阻塞函数
例如在async def内使用time.sleep或requests.get,会阻塞整个事件循环。
解决方案: 使用asyncio.to_thread将同步函数交给线程池执行。
import time
import asyncio
async def main():
await asyncio.to_thread(time.sleep, 2) # 在单独的线程中阻塞
问题3:asyncio.run与loop.run_until_complete的区别
asyncio.run(coro)是Python 3.7+的推荐方式,自动管理事件循环创建与关闭。loop.run_until_complete(coro)用于Python 3.6及以下,或需要手动控制事件循环时。
问答环节
Q:协程中能用return返回值吗?
A:可以,await coro()会返回该协程的return值,与普通函数一致。
何时该用协程,何时该用线程?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 大量网络请求(爬虫、API调用) | 协程 | 高并发,低内存开销 |
| CPU密集型计算(矩阵运算、视频处理) | 多进程(multiprocessing) |
利用多核CPU |
| 混合型任务(少量计算+大量IO) | 协程+线程池 | asyncio.to_thread平衡两者 |
| GUI应用(Tkinter、PyQt) | 协程 | 避免UI冻结 |
最后一条建议: 不要盲目追求协程,如果任务数量不多(如10个以内的IO请求),同步代码更简洁易读,当需要同时处理成百上千个连接时,协程的价值便会凸显。
通过本文的实战案例,你应该能掌握Python协程的核心用法。协程不是银弹,但它是处理IO密集型并发的利器,动手尝试上面的代码,你将更快理解异步编程的精髓。