Python案例中的协程如何使用?

wen python案例 2

Python案例中的协程如何使用?从基础到实战,一文掌握异步编程

目录导读

  1. 什么是协程?为什么它比线程更适合IO密集型任务?
  2. 协程的核心概念:asyncawait与事件循环
  3. 实战案例一:使用asyncio实现并发网络请求
  4. 实战案例二:协程与aiohttp构建高效爬虫
  5. 实战案例三:协程在文件读写与数据库操作中的应用
  6. 常见问题与避坑指南(含问答)
  7. 何时该用协程,何时该用线程?

什么是协程?为什么它比线程更适合IO密集型任务?

协程(Coroutine) 是Python中一种轻量级的并发编程方式,它允许函数在执行过程中暂停,并在未来某个时刻恢复执行,与线程不同,协程的切换完全由程序控制(而非操作系统),因此开销极低——一个进程中可以轻松创建数十万个协程,而线程通常只能支持几千个。

Python案例中的协程如何使用?

为什么协程在IO密集型任务中表现优异?
因为当程序等待网络响应、磁盘读取或数据库查询时,协程可以主动让出CPU,去执行其他协程,而线程在IO等待时会被操作系统挂起,造成上下文切换开销,举个例子:如果你需要同时下载100个网页,使用线程会导致频繁切换,而协程只需一个线程即可高效完成。

问答环节
Q:协程能替代线程吗?
A:不能完全替代,对于CPU密集型任务(如视频编码、数学计算),多线程或多进程更合适,协程擅长IO密集型任务,如网络请求、文件读写、数据库操作。


协程的核心概念:asyncawait与事件循环

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

问答环节
Qasyncio.sleep(1)time.sleep(1)有什么区别?
Atime.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秒内完成(受网络延迟影响)。

问答环节
Qaiohttprequests库能混用吗?
A:不建议。requests是同步阻塞库,在协程中使用会阻塞事件循环,导致并发失效,必须使用异步库(如aiohttphttpx)。


实战案例三:协程在文件读写与数据库操作中的应用

除了网络请求,协程也适用于文件系统操作和数据库查询,Python的aiofiles库提供异步文件读写,asyncpgaiomysql提供异步数据库驱动。

异步文件写入示例(需安装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.sleeprequests.get,会阻塞整个事件循环。

解决方案: 使用asyncio.to_thread将同步函数交给线程池执行。

import time
import asyncio
async def main():
    await asyncio.to_thread(time.sleep, 2)  # 在单独的线程中阻塞

问题3:asyncio.runloop.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密集型并发的利器,动手尝试上面的代码,你将更快理解异步编程的精髓。

抱歉,评论功能暂时关闭!