因为 semaphore 支持运行时动态调整并发数,而 waitgroup 仅能等待、channel 缓冲区容量不可变;需封装 x/sync/semaphore 并用 rwmutex 保护 limit 变更,acquire 时加读锁防指针过期,注意 acquire 参数为 int64 且 release 必须严格配对。

为什么用 semaphore 而不是 sync.WaitGroup 或 channel 做并发控制?
因为 sync.WaitGroup 只能等,不能限流;普通带缓冲 channel 虽能限流,但无法在运行时动态调整容量(cap(ch) 是只读的)。真正需要「运行中增减并发数」时,必须自己维护计数 + 同步原语,semaphore 就是这个角色的标准抽象。
用 golang.org/x/sync/semaphore 实现可调并发度的 Acquire / Release
官方 x/sync/semaphore 包提供信号量实现,但它本身不支持动态调整权重或最大许可数。所以得封装一层:把信号量实例和当前许可数一起管理,并用 sync.RWMutex 保护配置变更。
- 初始化时创建一个初始容量的
semaphore.Weighted - 暴露
SetLimit(int64)方法:先阻塞所有新请求(通过 acquire 0 个单位),再替换底层信号量 - 注意:旧信号量上正在等待的 goroutine 不会中断,但新 acquire 请求会走新信号量
- 示例关键片段:
type DynamicSemaphore struct { mu sync.RWMutex sem *semaphore.Weighted limit int64 } func (ds *DynamicSemaphore) SetLimit(limit int64) { ds.mu.Lock() defer ds.mu.Unlock() // 等待所有 pending acquire 完成(可选,取决于是否允许“软切换”) ds.sem = semaphore.NewWeighted(limit) ds.limit = limit }
Acquire 时如何避免被 SetLimit 中断导致死锁?
直接调用 sem.Acquire(ctx, 1) 在 SetLimit 替换信号量后,旧信号量可能已释放资源,但新信号量还没接管——这不是 bug,而是设计使然。真正要防的是:用户在 SetLimit 过程中调用 Acquire,而新信号量尚未就绪。
- 解决方案:所有公共方法都加
ds.mu.RLock()读锁,确保SetLimit写锁期间,Acquire不会拿到过期的ds.sem指针 - 不要在
Acquire里做重试逻辑——超时或取消由调用方ctx控制,信号量只负责“当前是否可进” - 如果
SetLimit(0),后续Acquire会立刻返回ErrSemaphoreFull,这是预期行为,不是错误
实际使用时最容易忽略的两个细节
一是 semaphore.Weighted 的 Acquire 参数是 int64,不是 int,传负数会 panic;二是 Release 必须与 Acquire 的数量严格匹配,漏调或重复调都会破坏计数。
- 建议封装
Run(func()) error方法,自动Acquire+defer Release,避免手动配对出错 - 监控指标别只看并发数,还要记录
acquire latency和wait queue length,否则调了 limit 也看不出效果 - 如果业务有“突发流量+快速降级”需求,
SetLimit最好配合atomic标记当前状态,避免多 goroutine 同时调用造成震荡
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











