基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Python中的协程(Coroutine)

知识点图片

Python 中的 协程 (Coroutine) 是一种比线程更加轻量级的并发编程方式。它允许你在单线程内通过“协作”的方式实现多任务并发,特别适合 I/O 密集型 任务(如网络请求、文件读写、数据库操作)。

自 Python 3.5 引入 asyncawait 关键字,以及 Python 3.4 引入标准库 asyncio 以来,协程已经成为 Python 现代并发编程的核心。

以下是关于 Python 协程的详细解析:


1. 核心概念

什么是协程?

  • 用户态线程:协程完全由程序(用户态)控制,不由操作系统内核管理。
  • 协作式调度:线程是“抢占式”的(操作系统决定什么时候切换),而协程是“协作式”的。协程自己决定何时暂停(挂起)并将控制权交还给事件循环,以便让其他协程运行。
  • 单线程并发:Python 的协程通常运行在一个线程中。这意味着同一时刻只有一个协程在执行代码,但利用 I/O 等待的时间切换任务,实现了极高的并发效率。

关键组件

  1. Event Loop (事件循环):协程的引擎。它是一个无限循环,负责监控 I/O 事件,调度和执行协程。
  2. Coroutine (协程对象):使用 async def 定义的函数调用后返回的对象。
  3. Task (任务):将协程包装成一个可以在事件循环中调度的对象(包含状态,如 pending, finished)。
  4. Future:代表一个未来会产生结果的对象(通常由底层库使用)。

2. 基本语法 (async / await)

这是 Python 3.5+ 的标准写法。

  • async def:定义一个协程函数。
  • await:挂起当前协程,等待后面的对象(Awaitable)执行完毕,并交出控制权。

最简单的例子 (Hello World)

python
import asyncio
import time

# 定义协程函数
async def say_after(delay, what):
    # await 后面必须跟一个可等待对象(如另一个协程)
    # asyncio.sleep 是非阻塞的睡眠,模拟 I/O 操作
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    # 串行执行(这不是并发)
    await say_after(1, 'hello')
    await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

# 运行顶层入口点
if __name__ == "__main__":
    # asyncio.run() 会自动创建事件循环并运行
    asyncio.run(main())

输出结果(耗时约 3 秒):

plaintext
started at 12:00:00
hello
world
finished at 12:00:03

3. 如何实现并发?

上面的例子中,await 会等待前一个任务完成才执行下一个,这实际上是串行的。要实现并发(同时等待多个任务),需要使用 asyncio.create_task()asyncio.gather()

并发执行示例

python
import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    # 创建任务(Task),这会将协程立即调度到事件循环中运行
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    # 等待两个任务都完成
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

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

输出结果(耗时约 2 秒,取决于最长的那个任务):

plaintext
started at 12:00:00
hello  (1秒后出现)
world  (2秒后出现)
finished at 12:00:02

原理task1 遇到 sleep 挂起,事件循环立即切换去执行 task2task2 也挂起,事件循环等待。1秒后 task1 恢复,2秒后 task2 恢复。


4. 协程 vs 线程 vs 进程

特性 进程 (Process) 线程 (Thread) 协程 (Coroutine)
内存占用 极大 (独立内存空间) 中等 (共享进程内存) 极小 (几 KB)
切换开销 极大 (内核态) 大 (内核态) 极小 (用户态)
并发能力 低 (受限于 CPU 核数) 中 (受限于系统资源) 极高 (单线程可支持数万协程)
数据安全 安全 (内存隔离) 不安全 (需加锁) 相对安全 (非抢占式,无竞态条件)
适用场景 CPU 密集型计算 I/O 密集型 (传统) 高并发 I/O 密集型

5. 常见误区与注意事项

1. 即使写了 async,代码也可能是阻塞的

绝对不能在协程中使用同步的阻塞 I/O,否则整个线程(包括事件循环)都会卡住,其他协程也无法运行。

  • 错误做法

    python
    import time
    import requests # requests 库是同步阻塞的
    
    async def bad_coroutine():
        # 这会卡死整个程序5秒,期间其他协程无法运行
        time.sleep(5) 
        # 这也会卡死
        requests.get("http://google.com") 
  • 正确做法
    使用异步库替代同步库。

    • time.sleep -> asyncio.sleep
    • requests -> aiohttphttpx (异步模式)
    • pymysql -> aiomysql

2. CPU 密集型任务怎么办?

协程本质是单线程。如果你在协程里跑一个死循环计算(如计算圆周率),整个 Loop 都会被卡住。
解决方案:使用 run_in_executor 将 CPU 密集型任务扔到线程池或进程池中去跑。

python
import asyncio
import time

# 一个耗时的同步函数
def blocking_io():
    print(f"start blocking_io at {time.strftime('%X')}")
    time.sleep(1) # 模拟阻塞
    print(f"blocking_io complete at {time.strftime('%X')}")

async def main():
    loop = asyncio.get_running_loop()
    
    # 在默认的线程池执行器中运行阻塞函数
    # 这样不会阻塞主线程的事件循环
    await loop.run_in_executor(None, blocking_io)

asyncio.run(main())

6. 实战:异步爬虫示例 (使用 aiohttp)

这是一个典型的协程应用场景:同时请求多个网页。

python
import asyncio
import aiohttp
import time

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

async def main():
    urls = [
        'http://www.python.org',
        'http://www.google.com',
        'http://www.baidu.com'
    ]

    async with aiohttp.ClientSession() as session:
        tasks = []
        for url in urls:
            # 创建任务列表
            task = asyncio.create_task(fetch(session, url))
            tasks.append(task)
        
        # gather 并发运行所有任务并收集结果
        results = await asyncio.gather(*tasks)
        
        for url, html in zip(urls, results):
            print(f"Read {len(html)} chars from {url}")

if __name__ == "__main__":
    start = time.time()
    # 注意:运行此代码需要安装 aiohttp (pip install aiohttp)
    # 某些环境(如 Jupyter)可能已有运行中的 loop,需特殊处理,
    # 标准脚本中直接用 asyncio.run(main()) 即可。
    try:
        asyncio.run(main())
    except RuntimeError:
        # 针对 Jupyter Notebook 等环境的兼容写法
        loop = asyncio.get_event_loop()
        loop.run_until_complete(main())
        
    print(f"Cost: {time.time() - start:.2f} seconds")

总结

  1. 协程是单线程的,通过事件循环在 I/O 等待时切换任务。
  2. 关键字async def 定义,await 调用,asyncio.run 启动。
  3. 核心优势:极高的 I/O 并发性能,资源消耗低。
  4. 核心禁忌:不要在协程中调用同步阻塞代码(如 time.sleeprequests),必须使用对应的异步库。
00:00
00:00