Semaphore
A semaphore is a synchronization mechanism used to manage access to a shared resource. In Effect, semaphores help control resource access or coordinate tasks within asynchronous, concurrent operations.
A semaphore acts as a generalized mutex, allowing a set number of permits to be held and released concurrently. Permits act like tickets, giving tasks or fibers controlled access to a shared resource. When no permits are available, tasks trying to acquire one will wait until a permit is released.
Creating a Semaphore
The Semaphore.make function initializes a semaphore with a specified number of permits.
Each permit allows one task to access a resource or perform an operation concurrently, and multiple permits enable a configurable level of concurrency.
Example (Creating a Semaphore with 3 Permits)
import { Effect, Semaphore } from "effect"
// Create a semaphore with 3 permitsconst mutex = Semaphore.make(3)
// The semaphore was created with exactly 3 permits availableconst acquired = await Effect.runPromise( mutex.pipe(Effect.flatMap((sem) => Semaphore.take(sem, 3))),)acquired // => 3withPermits
The withPermits method lets you specify the number of permits required to run an effect. Once the specified permits are available, it runs the effect, automatically releasing the permits when the task completes.
Example (Forcing Sequential Task Execution with a One-Permit Semaphore)
In this example, three tasks are started concurrently, but they run sequentially because the one-permit semaphore only allows one task to proceed at a time.
import { Effect, Semaphore } from "effect"
const task = Effect.gen(function* () { yield* Effect.log("start") yield* Effect.sleep("2 seconds") yield* Effect.log("end")})
const program = Effect.gen(function* () { const mutex = yield* Semaphore.make(1)
// Wrap the task to require one permit, forcing sequential execution const semTask = mutex.withPermits(1)(task).pipe(Effect.withLogSpan("elapsed"))
// Run 3 tasks concurrently, but they execute sequentially // due to the one-permit semaphore yield* Effect.all([semTask, semTask, semTask], { concurrency: "unbounded", })})
await Effect.runPromise(program) // => undefined/*Output:timestamp=... level=INFO fiber=#1 message=start elapsed=3mstimestamp=... level=INFO fiber=#1 message=end elapsed=2010mstimestamp=... level=INFO fiber=#2 message=start elapsed=2012mstimestamp=... level=INFO fiber=#2 message=end elapsed=4017mstimestamp=... level=INFO fiber=#3 message=start elapsed=4018mstimestamp=... level=INFO fiber=#3 message=end elapsed=6026ms*/Example (Using Multiple Permits to Control Concurrent Task Execution)
In this example, we create a semaphore with five permits and use withPermits(n) to allocate a different number of permits for each task:
import { Effect, Semaphore } from "effect"
const program = Effect.gen(function* () { const mutex = yield* Semaphore.make(5)
const tasks = [1, 2, 3, 4, 5].map((n) => mutex .withPermits(n)(Effect.delay(Effect.log(`process: ${n}`), "2 seconds")) .pipe(Effect.withLogSpan("elapsed")), )
yield* Effect.all(tasks, { concurrency: "unbounded" })})
await Effect.runPromise(program) // => undefined/*Output:timestamp=... level=INFO fiber=#1 message="process: 1" elapsed=2011mstimestamp=... level=INFO fiber=#2 message="process: 2" elapsed=2017mstimestamp=... level=INFO fiber=#3 message="process: 3" elapsed=4020mstimestamp=... level=INFO fiber=#4 message="process: 4" elapsed=6025mstimestamp=... level=INFO fiber=#5 message="process: 5" elapsed=8034ms*/