游乐游手机版
首页/编程语言/文章详情

Golang 如何限制并发数

时间:2026-04-28 18:34
Golang 如何限制并发数 用 sync Semaphore(Go 1 21+)最稳妥 从Go 1 21开始,标准库自带的 sync Semaphore 就成了生产环境的首选方案。它可不是一个简单的计数器,而是一个功能完备的信号量实现,支持权重、上下文取消,并且是panic安全的。 不过,用起来有

Golang 如何限制并发数

Golang 如何限制并发数

sync.Semaphore(Go 1.21+)最稳妥

从Go 1.21开始,标准库自带的 sync.Semaphore 就成了生产环境的首选方案。它可不是一个简单的计数器,而是一个功能完备的信号量实现,支持权重、上下文取消,并且是panic安全的。

不过,用起来有几个细节必须注意,否则很容易踩坑:

  • 初始化必须传 int64:比如你想限制8个并发,得写成 semaphore.NewWeighted(8)。如果传一个普通的 8int类型),编译器会直接报错。
  • sem.Acquire(ctx, 1) 必须检查返回的 error —— 如果上下文(ctx)已经取消或超时,这个方法会立刻返回 context.Canceledcontext.DeadlineExceeded。这时候,可千万别再执行业务逻辑了。
  • defer sem.Release(1) 要写在 goroutine 的最外层:这个习惯能保证,即使你的 doWork() 函数 panic 了,defer 语句依然会执行,令牌不会泄漏。
  • 别把 ctx 只用在 Acquire:如果你的业务逻辑本身需要发起 HTTP 请求,记得把同一个 ctx 也传给 http.Client.Do()。否则,可能会出现信号量等到了,但请求却卡死在 TCP 握手阶段的尴尬情况。

chan struct{} 模拟信号量(兼容所有 Go 版本)

对于简单的场景,用通道(channel)模拟信号量是个轻量且零依赖的选择,所有 Go 版本都兼容。但这个方法有个致命弱点:极易翻车——只要漏释放一次令牌,后续所有任务就会永久阻塞。

想用对,得盯紧下面几点:

  • 声明必须带缓冲:正确写法是 sem := make(chan struct{}, 5)。如果写成无缓冲的 make(chan struct{}),那就完全失去了限流的意义。
  • 获取和释放必须严格配对sem <- struct{}{} 是占位,<-sem 是归还。另外,别用 len(sem) 来判断是否能进入,它返回的是已占用的数量,可用数应该是 cap(sem) - len(sem)
  • 安全写法是 defer func() { <-sem }():但要注意,闭包捕获的变量需要显式传入。比如在循环里启动 goroutine,应该写成 go func(s chan struct{}) { s <- struct{}{}; defer func() { <-s }(); ... }(sem),否则闭包捕获的可能是循环的最终值,导致所有任务挤进同一个“槽位”。
  • 别在 select 里只写 case <-sem: 就开干:万一任务在注册 defer 之前就 panic 了,令牌可就找不回来了。

HTTP 请求并发限制,关键位置在哪

限制 HTTP 请求并发,关键在于把限流逻辑放在真正发起网络调用的那一刻之前,而不是在启动 goroutine 时,更不是在 for 循环外面。很多人把信号量放错了位置,结果下游服务的 QPS 照样被打满。

  • 正确位置:在 http.NewRequest() 之后,client.Do() 之前。这样,即使请求失败、超时、甚至 panic,defer 也能保证令牌被安全归还。
  • 错误示范for range urls { go func() { sem <- struct{}{}; ... }() } —— 这里的 sem <- 虽然在 goroutine 内部,但如果 goroutine 启动过快,实际并发连接数依然不可控。更糟的是把信号量操作放在 for 循环里、goroutine 外部,那只是限制了 goroutine 的启动速度,对网络并发毫无约束。
  • http.TransportMaxConnsPerHost 不等于并发请求数:这个参数控制的是 TCP 连接池的复用上限,它和信号量是协作关系,而非替代关系。通常建议将信号量设为你的业务目标并发数(例如10),而将 MaxConnsPerHost 设为一个略大的值(例如15),以允许合理的连接复用。

什么时候该换 worker pool,而不是只靠信号量

信号量只回答了“最多几个任务同时跑”的问题,但它不关心“谁来跑”、“跑几次”以及“任务积压了怎么办”。一旦遇到以下几种情况,就该考虑升级到更复杂的 worker pool 模式了:

  • 需要拒绝新任务,而不是无限排队等待:信号量会阻塞等待,而像 ants 这样的 worker pool 库,可以通过配置 ants.WithNonblocking(true) 来直接丢弃无法立即处理的任务。
  • 需要统计实时指标:比如当前正在运行的任务数、排队中的任务数量、任务平均耗时等。裸信号量无法提供这些观测维度。
  • 任务执行时间差异极大,存在长期挂起的风险:比如调用某个不稳定的第三方接口。worker pool 可以内置超时和熔断机制,而信号量只能被动等待任务自己结束。
  • 你已经在使用 ants 或类似库:这类库的 NewPool(n) 本质上封装了信号量、goroutine 预热以及 panic 捕获,比自己手写更稳健。但要注意,如果 worker 数量(n)远小于任务总量,且没有设置合理的任务队列缓冲,可能会导致大量任务被静默丢弃。
来源:https://www.php.cn/faq/2384863.html
上一篇如何在 Pandas 中按自然周分组且不跨月 下一篇C++如何获取硬盘分区的详细挂载信息 _ filesystem库实战【实战】
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
如何在ThinkPHP中实现定时任务与命令行调度方法
编程语言 · 2026-07-04

如何在ThinkPHP中实现定时任务与命令行调度方法

用ThinkPHP实现定时任务时,很多开发者第一步就卡在命令行报错上,直接输入php think your:command却无法识别——这种情况绝大多数是因为命令类的注册方式存在问题。下面先梳理几个核心要点。 ThinkPHP 6 中 think 命令如何正确触发自定义指令 直接运行 php thi

ThinkPHP API接口防重放攻击实现方法
编程语言 · 2026-07-04

ThinkPHP API接口防重放攻击实现方法

先说几个核心判断:API防重放攻击这件事,做对了是道防火墙,做错了就是个心理安慰。很多开发者到踩坑了才明白——验签这东西,放错位置、漏掉字段、存错nonce,每一环都能让整个安全体系直接归零。 验签必须放在中间件里,不能在控制器里写 ThinkPHP 的请求生命周期中,中间件是唯一能在路由匹配、参数

ThinkPHP文件上传必须验证扩展名安全必要性分析
编程语言 · 2026-07-04

ThinkPHP文件上传必须验证扩展名安全必要性分析

在使用ThinkPHP进行文件上传时,ext扩展名验证通常是开发者首先接触的关键环节。但你真的了解它的实际工作原理吗?它仅比对文件名后缀,而不读取文件内容,甚至对空格和大小写都极其敏感。更为重要的是——它是TP文件上传验证五层防线中不可忽视的第一道关卡,一旦配置遗漏,整个validate验证链将直接

ThinkPHP关联模型自动写入与更新使用教程
编程语言 · 2026-07-04

ThinkPHP关联模型自动写入与更新使用教程

需要明确的是,ThinkPHP关联模型并没有提供所谓的“自动写入 更新”魔法开关。所谓的“自动”功能,实际上都需要开发者手动编写配置逻辑才能生效。核心原则在于:主模型和从模型必须分开独立处理,时间戳字段和业务字段需依靠修改器或钩子接管;批量操作则要规规矩矩地绕过模型逻辑来执行——只有理解透彻这些要点

BoxLayout中仅居中一个组件其他默认左对齐
编程语言 · 2026-07-04

BoxLayout中仅居中一个组件其他默认左对齐

在 Java Swing 中使用 BoxLayout 的 Y_AXIS 方向布局时,很多初学者容易掉进一个常见陷阱:希望将某个组件单独设置为中心对齐,但当调用 `setAlignmentX(CENTER_ALIGNMENT)` 后,却发现其他组件也跟着发生了偏移,完全达不到预期效果。实际上,关键之处