当前位置:首页 > 文章列表 > 文章 > python教程 > Python asyncio.Semaphore 怎么限制并发任务数量

Python asyncio.Semaphore 怎么限制并发任务数量

来源:17golang原创 2026-11-05 14:55:51 0浏览 收藏

批量请求、文件处理或异步任务一多,最容易出现的误会是:已经用 asyncio.gather() 创建了任务,就等于已经限制了并发。实际上,任务可以一次性创建很多个;真正需要限制的是同一时刻有多少个协程进入那段受限工作。asyncio.Semaphore 用一个内部计数器完成这件事:拿到许可后计数减少,许可用完时后续协程等待,退出受限区时再归还许可。

官方文档:https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore

如果目标是“最多同时运行 N 个协程的某一段工作”,就把 asyncio.Semaphore(N) 放在共享任务函数外部,用 async with semaphore 包住受限区;需要手动控制时,必须把 release() 放进 finally。

先分清:限制任务数量,还是限制受限区

我在批量处理场景里更愿意把“任务总数”和“并发上限”拆成两个问题。任务总数描述要处理多少项,并发上限描述同一时刻允许多少项占用下游资源。比如有 1000 个 URL 待处理,可以创建 1000 个协程任务,但让每个任务在访问外部服务前先竞争一个容量为 20 的信号量。

信号量的初始值就是内部计数器的初始值。acquire() 成功时计数减一;当计数已经为零,新的 acquire() 会等待;release() 会把计数加一,并可能唤醒等待中的任务。初始值不能小于零,默认值是 1。它是 asyncio 原语,不是用来同步操作系统线程的锁。

asyncio.Semaphore 连接任务入口、内部计数器和受限工作的静态关系图
图1:asyncio.Semaphore 的结构示意图,重点看任务入口与内部计数器之间的许可关系,不表示实际运行截图。

最小写法:用 async with 自动归还许可

最稳定的入口是把信号量作为多个任务共享的对象,然后只包住真正需要限流的代码。下面的例子仍然一次创建多个任务,但每个任务只有进入 async with limit 后才占用一个并发名额。

import asyncio


async def fetch_one(item: int, limit: asyncio.Semaphore) -> str:
    # 只有 async with 内的工作受到并发上限约束。
    async with limit:
        # 这里替换成真实的异步 I/O;sleep 只是模拟等待。
        await asyncio.sleep(0.05)
        return f"完成 {item}"


async def main() -> None:
    # 允许同时进入受限区的任务最多为 3 个。
    limit = asyncio.Semaphore(3)
    tasks = [
        asyncio.create_task(fetch_one(item, limit))
        for item in range(10)
    ]

    # gather 等待所有任务;Semaphore 负责限制受限区,而不是限制任务创建。
    results = await asyncio.gather(*tasks)
    print(results)


asyncio.run(main())

这里的关键不是 range(10),而是所有任务共享同一个 limit。如果每次调用 fetch_one() 都重新创建一个信号量,任务之间就各自拥有计数器,并不能形成全局上限。

手动 acquire 时,把 release 放进 finally

async with 已经覆盖了大多数情况,但有时需要在取得许可前后加入额外判断,或者必须把“等待许可”和“执行工作”分开记录。这时可以手动调用 acquire(),但配对关系不能靠记忆维护。

import asyncio


async def fetch_with_cleanup(item: int, limit: asyncio.Semaphore) -> str:
    # acquire 可能等待,因此许可取得前不应把任务算作正在处理。
    await limit.acquire()
    try:
        # 业务异常、CancelledError 或正常返回都会经过 finally。
        await asyncio.sleep(0.05)
        if item == 4:
            raise RuntimeError("模拟单项失败")
        return f"完成 {item}"
    finally:
        # 无论工作如何结束,都把这一次成功取得的许可归还。
        limit.release()

顺序很重要:只有 acquire() 成功返回后,才进入需要归还的 try 区域。若在等待许可阶段被取消,并没有拿到许可,也就不应该额外调用 release()。

asyncio.Semaphore 在异常或取消时通过 try finally 调用 release 的静态关系图
图2:许可清理结构示意图,展示异常或取消如何落到 finally 中的 release,不表示实际执行结果。

超时应该包住 acquire,而不是假设它自带 timeout

asyncio 的同步原语方法本身不接收 timeout 参数。如果等待许可也必须有上限,可以用 asyncio.wait_for() 包住 acquire()。这能把“资源迟迟不可用”和“拿到资源后业务执行超时”区分开。

import asyncio


async def try_fetch(item: int, limit: asyncio.Semaphore) -> str | None:
    try:
        # 这里只给等待许可设置 0.2 秒上限。
        await asyncio.wait_for(limit.acquire(), timeout=0.2)
    except asyncio.TimeoutError:
        # acquire 超时意味着没有拿到许可,因此这里不能 release。
        return None

    try:
        # 许可已经拿到,后续工作必须由 finally 负责归还。
        await asyncio.sleep(0.05)
        return f"完成 {item}"
    finally:
        # 只释放本协程实际持有的许可。
        limit.release()

检查当前是否不能立即取得许可,可以读取 limit.locked()。它返回 True 时表示当前不能立即获取,但这不是一个预留名额的操作;检查结果和真正的 acquire() 之间仍可能有其他任务改变状态,所以它适合记录或调试,不适合代替获取许可。

Semaphore、BoundedSemaphore 和过度 release

普通 Semaphore 允许调用次数多于成功的 acquire() 次数,内部计数器可能因此超过初始值。这个特性有时便于表达“可增加的容量”,但如果代码把许可释放错了,问题不会立刻暴露成异常。

asyncio.BoundedSemaphore 会记住初始值;当 release() 让计数超过这个初始值时,它会抛出 ValueError。如果你的许可数量应该严格守恒,使用它能更早暴露配对错误。不过它依然需要正确的 acquire() 与 release() 生命周期,不能把异常交给信号量替你修复。

场景建议注意点
限制同一事件循环中的协程工作Semaphore(N)所有任务共享同一个实例
许可必须和初始容量严格配对BoundedSemaphore(N)多余 release 会抛出 ValueError
等待许可不能无限期阻塞wait_for(acquire(), timeout=...)超时后没有取得许可,不要 release
需要保护操作系统线程不要把 asyncio.Semaphore 当线程锁asyncio 原语不是线程安全同步工具

最后用一张清单检查并发上限

  • 信号量是否在任务之间共享,而不是在每次函数调用中重新创建?
  • 是否只把真正需要限流的 I/O 或资源占用放进 async with?
  • 手动调用 acquire() 后,是否只在成功取得许可后进入 try/finally?
  • 异常、取消和正常返回是否都会执行 release()?
  • 等待许可超时后,是否避免释放一个实际上没有取得的许可?
  • 是否把“同时运行的协程数”与线程池、进程池或下游服务的其他容量限制混在了一起?

把这几个问题逐项回答清楚,通常就能判断 asyncio.Semaphore 是否放在了正确的边界上。它控制的是协程进入共享受限区的许可数量;任务如何创建、异常如何传播和业务如何重试,仍然需要由调用方自己设计。

相关问题

Semaphore 的默认并发数是多少?

构造函数默认值是 1,因此默认表现接近一次只允许一个任务进入受限区;生产代码应根据下游容量明确传入数值。

为什么 release 后并发数看起来超过了设定值?

常见原因是重复调用了 release()。普通 Semaphore 允许过度释放;需要尽早发现配对错误时可考虑 BoundedSemaphore。

asyncio.Semaphore 能限制线程池任务吗?

本文范围内不能把它当作操作系统线程同步工具。它面向 asyncio 任务;线程同步应选择线程模型对应的同步原语,并单独设计跨模型边界。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Java String.indent 为什么会自动补换行符Java String.indent 为什么会自动补换行符
上一篇
Java String.indent 为什么会自动补换行符
Python ContextDecorator 重复调用时怎么避免上下文状态串线
下一篇
Python ContextDecorator 重复调用时怎么避免上下文状态串线
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    893次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    896次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    981次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    968次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    794次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码