Python协程

Python 中的协程,是一种用于协作式多任务处理的程序组件。它允许你在单个线程内,通过主动让出控制权来实现并发,特别适合处理大量 I/O 密集型任务,可以避免传统多线程带来的上下文切换开销和复杂的锁问题。

下面我们从概念到实践,系统地讲解 Python 协程。


一、什么是协程

  • 常规函数:调用后一口气执行完毕,期间不会主动暂停。
  • 协程函数 :执行过程中可以暂停并保存当前状态,把控制权交还给调度器,之后再从暂停的地方恢复执行。

这种"自己让出执行权"的方式就叫做协作式。它不同于线程那种由操作系统强制切换的抢占式多任务。

Python 从 yield 生成器演化而来,最终在 3.5 引入了 async def / await 语法,正式确立了原生协程。


二、快速体验

python 复制代码
import asyncio

async def hello():
    print("Hello")
    await asyncio.sleep(1)   # 模拟 I/O,主动让出控制权
    print("World")

asyncio.run(hello())
  • async def 定义一个协程函数,调用它并不会立刻执行,而是返回一个协程对象
  • await 用来等待另一个协程或可等待对象,期间挂起当前协程。
  • asyncio.sleep(1) 是一个异步的休眠,不会阻塞整个线程。
  • asyncio.run() 用来运行顶层协程并管理事件循环。

三、核心概念

3.1 可等待对象(Awaitable)

能在 await 后面使用的对象有三种:

  • 协程对象async def 函数的返回值。
  • 任务(Task) :用 asyncio.create_task() 包装协程,可以并发调度。
  • Future:底层占位符,代表一个未来才会完成的结果(Task 是一种 Future)。
python 复制代码
async def nested():
    return 42

async def main():
    # 直接 await 协程
    print(await nested())

    # 包装成任务,立即开始并发执行
    task = asyncio.create_task(nested())
    print(await task)

asyncio.run(main())

3.2 事件循环(Event Loop)

事件循环是协程的调度核心,它不断地检查哪些任务可以继续执行,循环往复。asyncio.run() 会创建、运行并清理事件循环。在单线程里,所有协程都由这个循环统一调度。


四、并发执行

同时发起多个 I/O 操作,等其全部完成,这正是协程的优势。

python 复制代码
import asyncio
import time

async def download(name, duration):
    print(f"开始下载 {name}")
    await asyncio.sleep(duration)   # 模拟下载耗时
    print(f"{name} 下载完成")
    return f"{name} 的数据"

async def main():
    start = time.time()
    # 依次执行,约 4 秒
    # await download("文件1", 2)
    # await download("文件2", 2)

    # 并发执行,约 2 秒
    results = await asyncio.gather(
        download("文件1", 2),
        download("文件2", 2),
    )
    print(results)
    print(f"总耗时 {time.time() - start:.2f} 秒")

asyncio.run(main())

asyncio.gather() 会并发等待多个协程,全部完成后返回结果列表。

若某个协程失败,可以通过 return_exceptions=True 让异常不会被立即抛出,而是作为结果返回。


五、创建与管理任务

asyncio.create_task() 会将协程包装成 Task,并立刻注册到事件循环中执行。你可以在稍后等待它。

python 复制代码
async def fire_and_forget():
    task = asyncio.create_task(download("文件3", 3))
    # 这里可以做其他事,task 在后台运行
    await asyncio.sleep(0.1)
    # 再等待 task
    await task

常用任务管理方法:

  • task.cancel() 取消任务(会引发 CancelledError)。
  • asyncio.wait_for(aw, timeout) 给协程加超时限制。
  • asyncio.shield(aw) 保护协程不被外部取消。

六、异步生成器与异步迭代器

async def 里使用 yield 就得到异步生成器,用 async for 遍历。

python 复制代码
import asyncio

async def countdown(n):
    while n > 0:
        await asyncio.sleep(1)
        yield n
        n -= 1

async def main():
    async for num in countdown(3):
        print(num)

asyncio.run(main())

异步生成器内部可以包含 await,每次 yield 前都可以挂起,非常适合流式处理异步数据源。

异步推导式

python 复制代码
result = [x async for x in async_iter]
result = {x async for x in async_iter}
result = {key: val async for key, val in async_items}

七、同步原语与队列

asyncio 提供了协程版本的锁、信号量、事件和条件等,用于保护共享资源或协调任务。

python 复制代码
lock = asyncio.Lock()

async def safe_update(data):
    async with lock:
        # 修改共享状态
        pass

asyncio.Queue 可用于生产者-消费者模式:

python 复制代码
queue = asyncio.Queue(maxsize=10)

async def producer():
    for i in range(5):
        await queue.put(i)
        await asyncio.sleep(0.5)

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

八、与线程的交互

协程在单线程里运行,但有时需要调用已有的同步阻塞库。asyncio.to_thread() 可以将阻塞函数扔到线程池中异步执行。

python 复制代码
import time
import asyncio

def blocking_io():
    time.sleep(1)
    return "结果"

async def main():
    result = await asyncio.to_thread(blocking_io)
    print(result)

asyncio.run(main())

loop.run_in_executor() 则是更底层的接口,同样可以指定线程或进程池。


九、常见陷阱与最佳实践

  1. async 函数里不要用同步阻塞调用

    例如 time.sleep() 会卡住整个事件循环,应使用 await asyncio.sleep()

  2. 不要忘记 await 任务

    如果只创建了 create_task() 但从不对它 await,可能看不到异常,且程序可能提前退出。最好保存 task 引用,并在最后等待。

  3. 避免混用 asyncio.run()

    在已有事件循环的环境(如 Jupyter Notebook,或部分 Web 框架)里反复调用会出错,可以用 await 代替。

  4. 并发度控制

    如果同时发起成千上万个请求,可以使用 asyncio.Semaphore 限制并发数,防止对方服务器过载或本地资源耗尽。

    python 复制代码
    sem = asyncio.Semaphore(10)
    async def fetch(url):
        async with sem:
            # 发送请求
            pass
  5. 取消处理

    长时间运行的任务应当响应用 try...except asyncio.CancelledError 处理取消请求,做必要的清理工作。

  6. Python 版本差异

    Python 3.7+ 推荐使用 asyncio.run();3.8 引入了 asyncio.to_thread();3.11 带来了 TaskGroup,更优雅地管理任务生命周期。


十、实战:简易异步爬虫骨架

python 复制代码
import asyncio
import aiohttp

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

async def main(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        pages = await asyncio.gather(*tasks, return_exceptions=True)
        for url, page in zip(urls, pages):
            if isinstance(page, Exception):
                print(f"{url} 失败: {page}")
            else:
                print(f"{url} 返回长度: {len(page)}")

urls = ["https://example.com"] * 5
asyncio.run(main(urls))

总结

  • 协程函数async def 定义,await 挂起等待。
  • 事件循环是调度中心,单线程并发关键。
  • 并发执行 主要靠 asyncio.gather()create_task()
  • 异步生成器异步推导式让你能流式处理和收集异步数据。
  • 必须注意线程安全阻塞调用取消处理等细节。

掌握这些内容,你就可以在 Python 中高效地编写异步 I/O 程序了。对于 CPU 密集型任务,仍需考虑多进程或其他方案,但在 I/O 密集型场景下,协程是极轻量、易维护的并发利器。

相关推荐
forEverPlume1 小时前
Go语言如何防SQL注入_Go语言SQL注入防护教程【精选】
jvm·数据库·python
m0_617881421 小时前
mysql升级后日志文件如何处理_mysql日志迁移说明
jvm·数据库·python
baidu_340998821 小时前
JavaScript中类的装饰器提案在属性与方法上的应用
jvm·数据库·python
zhangzeyuaaa1 小时前
Python多进程同步与共享内存完全指南:从Lock到分布式共享
开发语言·分布式·python
最贪吃的虎1 小时前
MIT新论文:Hyperloop Transformers
人工智能·python·语言模型·langchain
꧁细听勿语情꧂2 小时前
用队列实现栈、用栈实现队列,树、二叉树、满二叉树、完全二叉树,堆、向下向上调整算法、出堆入堆、堆排序
c语言·开发语言·数据结构·算法
weixin_381288182 小时前
mysql如何配置多实例运行环境_单机部署多个数据库服务
jvm·数据库·python
m0_734949792 小时前
PHP怎么使用Eloquent Attribute Synthesis属性合成_Laravel多源数据融合【指南】
jvm·数据库·python
香山上的麻雀10082 小时前
由 Rust 开发的能大幅降低LLM token消耗的高性能 CLI 代理工具 rtk
开发语言·后端·rust