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 的用法和代码示例没有在这次改写里逐段重新运行验证,使用前请先在你的版本上跑一遍;「并发度不是速率」的结论是对其语义的说明,不是实测结果。