Effect 里怎么用 Semaphore 限制并发数?
一百个 API 调用同时被发起,对方却只允许你同一时刻跑 5 个。用自旋的 while 去数在跑的数量,通常会漏掉取消和排队语义。
用 Semaphore.make(5) 创建 5 个许可,再用 limit.withPermits(1) 包裹每个调用:执行期间占用一个许可,许可用完时后来者排队等待。 注意它限制的是同时进行的数量,不是每秒多少次。
这个原语还有一个特例,许可数为 1 时就是互斥锁。下面讲用法、它和 concurrency 选项的分工,以及为什么它不能当限速器。这里用的是 Effect v4 的 API,其中用到的 Effect.forEach 的 concurrency 选项见 Effect 并发与资源安全。
Semaphore 怎么写?
Semaphore 持有 n 个许可。用 semaphore.withPermits(1) 包裹一个 Effect,它执行期间占用一个许可;许可用完时,后来者排队等待。
import { Effect, Semaphore } from "effect"
declare const callApi: (id: number) => Effect.Effect<string>
export const program = Effect.gen(function*() {
const limit = yield* Semaphore.make(5)
const limited = (id: number) => callApi(id).pipe(limit.withPermits(1))
return yield* Effect.forEach(
Array.from({ length: 100 }, (_, i) => i),
limited,
{ concurrency: "unbounded" }
)
})这一百个调用同时被发起,但任一时刻最多 5 个在执行。
它和 forEach 的 concurrency 选项有什么区别?
concurrency 选项限制的是一次 forEach 的并发度。Semaphore 的价值在于跨多个调用点共享同一个上限:只要这些调用点用的是同一个 Semaphore,它们合起来也不会超过上限。
许可数为 1 能当锁用吗?
可以。许可数为 1 的 Semaphore 就是互斥锁,适合保护「同一时刻只能有一个在做」的临界区,例如串行化对某个外部资源的写入。
为什么它不能当限速器?
因为 Semaphore 限制的是同时进行的数量(并发度),不是每秒多少次(速率)。它只数「正在执行的」,不数时间。
要限速需要令牌桶一类的工具,见 Effect 限流:RateLimiter。
要共享的是值而不是许可,该用什么?
那是 Ref 的场景,见 多个 fiber 共享状态,Ref、Queue、PubSub、Semaphore 怎么选?。
本文基于 effect 4.0.5 的 v4 API。Semaphore 的用法和代码示例没有在这次改写里逐段重新运行验证,使用前请先在你的版本上跑一遍;「并发度不是速率」的结论是对其语义的说明,不是实测结果。