Effect -- Concurrency

基础并发

并发选项

Effect 提供了用于管理副作用执行方式的选项,尤其专注于控制并发运行的副作用数量。

type Options = {
  readonly concurrency?: Concurrency
}

concurrency 选项用于指定并发级别,支持以下取值:

type Concurrency = number | "unbounded" | "inherit"

下面将详细介绍每种配置方式。

并发选项的适用范围

本文示例使用 Effect.all 函数,但这些选项同样适用于许多其他 Effect API。

顺序执行(默认)

默认情况下,若未指定任何并发选项,副作用将按顺序执行,即一个执行完成后下一个才开始。

示例(顺序执行)

import { Effect, Duration } from "effect"

// 辅助函数:模拟带延迟的任务
const makeTask = (n: number, delay: Duration.DurationInput) =>
  Effect.promise(
    () =>
      new Promise<void>((resolve) => {
        console.log(`start task${n}`) // 任务开始时打印日志
        setTimeout(() => {
          console.log(`task${n} done`) // 任务完成时打印日志
          resolve()
        }, Duration.toMillis(delay))
      })
  )

const task1 = makeTask(1, "200 millis")
const task2 = makeTask(2, "100 millis")

const sequential = Effect.all([task1, task2])

Effect.runPromise(sequential)
/*
输出:
start task1
task1 done
start task2 <-- 仅在 task1 完成后才启动 task2
task2 done
*/

数值并发

通过将 concurrency 设置为具体数字,可以控制并发运行的副作用数量。例如 concurrency: 2 表示最多允许两个副作用同时运行。

示例(限制 2 个并发任务)

import { Effect, Duration } from "effect"

// 辅助函数:模拟带延迟的任务
const makeTask = (n: number, delay: Duration.DurationInput) =>
  Effect.promise(
    () =>
      new Promise<void>((resolve) => {
        console.log(`start task${n}`) // 任务开始时打印日志
        setTimeout(() => {
          console.log(`task${n} done`) // 任务完成时打印日志
          resolve()
        }, Duration.toMillis(delay))
      })
  )

const task1 = makeTask(1, "200 millis")
const task2 = makeTask(2, "100 millis")
const task3 = makeTask(3, "210 millis")
const task4 = makeTask(4, "110 millis")
const task5 = makeTask(5, "150 millis")

const numbered = Effect.all([task1, task2, task3, task4, task5], {
  concurrency: 2
})

Effect.runPromise(numbered)
/*
输出:
start task1
start task2 <-- 活跃任务:task1、task2
task2 done
start task3 <-- 活跃任务:task1、task3
task1 done
start task4 <-- 活跃任务:task3、task4
task4 done
start task5 <-- 活跃任务:task3、task5
task3 done
task5 done
*/

无界并发

当设置 concurrency: "unbounded" 时,并发运行的副作用数量没有限制。

示例(无界并发)

import { Effect, Duration } from "effect"

// 辅助函数:模拟带延迟的任务
const makeTask = (n: number, delay: Duration.DurationInput) =>
  Effect.promise(
    () =>
      new Promise<void>((resolve) => {
        console.log(`start task${n}`) // 任务开始时打印日志
        setTimeout(() => {
          console.log(`task${n} done`) // 任务完成时打印日志
          resolve()
        }, Duration.toMillis(delay))
      })
  )

const task1 = makeTask(1, "200 millis")
const task2 = makeTask(2, "100 millis")
const task3 = makeTask(3, "210 millis")
const task4 = makeTask(4, "110 millis")
const task5 = makeTask(5, "150 millis")

const unbounded = Effect.all([task1, task2, task3, task4, task5], {
  concurrency: "unbounded"
})

Effect.runPromise(unbounded)
/*
输出:
start task1
start task2
start task3
start task4
start task5
task2 done
task4 done
task5 done
task1 done
task3 done
*/

继承并发

当设置 concurrency: "inherit" 时,并发级别将从周围的上下文继承。可通过 Effect.withConcurrency(number | "unbounded") 设置该上下文。若未提供上下文,默认值为 "unbounded"

示例(从上下文继承并发)

import { Effect, Duration } from "effect"

// 辅助函数:模拟带延迟的任务
const makeTask = (n: number, delay: Duration.DurationInput) =>
  Effect.promise(
    () =>
      new Promise<void>((resolve) => {
        console.log(`start task${n}`) // 任务开始时打印日志
        setTimeout(() => {
          console.log(`task${n} done`) // 任务完成时打印日志
          resolve()
        }, Duration.toMillis(delay))
      })
  )

const task1 = makeTask(1, "200 millis")
const task2 = makeTask(2, "100 millis")
const task3 = makeTask(3, "210 millis")
const task4 = makeTask(4, "110 millis")
const task5 = makeTask(5, "150 millis")

// 以 concurrency: "inherit" 运行所有任务
// 默认为 "unbounded"
const inherit = Effect.all([task1, task2, task3, task4, task5], {
  concurrency: "inherit"
})

Effect.runPromise(inherit)
/*
输出:
start task1
start task2
start task3
start task4
start task5
task2 done
task4 done
task5 done
task1 done
task3 done
*/

若使用 Effect.withConcurrency,并发配置将调整为指定选项。

示例(设置并发选项)

import { Effect, Duration } from "effect"

// 辅助函数:模拟带延迟的任务
const makeTask = (n: number, delay: Duration.DurationInput) =>
  Effect.promise(
    () =>
      new Promise<void>((resolve) => {
        console.log(`start task${n}`) // 任务开始时打印日志
        setTimeout(() => {
          console.log(`task${n} done`) // 任务完成时打印日志
          resolve()
        }, Duration.toMillis(delay))
      })
  )

const task1 = makeTask(1, "200 millis")
const task2 = makeTask(2, "100 millis")
const task3 = makeTask(3, "210 millis")
const task4 = makeTask(4, "110 millis")
const task5 = makeTask(5, "150 millis")

// 以 concurrency: "inherit" 运行任务
// 将继承周围的上下文
const inherit = Effect.all([task1, task2, task3, task4, task5], {
  concurrency: "inherit"
})

// 设置并发限制为 2
const withConcurrency = inherit.pipe(Effect.withConcurrency(2))

Effect.runPromise(withConcurrency)
/*
输出:
start task1
start task2 <-- 活跃任务:task1、task2
task2 done
start task3 <-- 活跃任务:task1、task3
task1 done
start task4 <-- 活跃任务:task3、task4
task4 done
start task5 <-- 活跃任务:task3、task5
task3 done
task5 done
*/

中断机制

Effect 中的所有副作用都通过纤程(fiber)执行。若未手动创建纤程,则由正在使用的操作(若为并发操作)或 Effect 运行时系统自动创建。

每次运行副作用时都会创建一个纤程。并发运行副作用时,每个并发副作用都会对应一个纤程。

核心概念总结

  • Effect:高层概念,描述带有副作用的计算。具有惰性和不可变性,代表可能产生值或失败但不会立即执行的计算。
  • 纤程(Fiber):代表 Effect 的运行实例。可被中断或等待以获取结果,是控制和交互正在进行的计算的方式。

纤程可通过多种方式中断,下面将介绍 Effect 中中断纤程的常见场景和示例。

interrupt 方法

可通过在特定纤程上调用 Effect.interrupt 副作用来中断该纤程。

该副作用用于显式中断其所在的纤程。执行后会立即停止纤程操作,并捕获中断详情(如纤程 ID 和启动时间)。若通过 runPromiseExit 等函数运行副作用,可在 Exit 类型中观察到中断结果。

示例(无中断)

程序正常运行,无中断,打印任务的开始和完成日志。

import { Effect } from "effect"

const program = Effect.gen(function* () {
  console.log("start")
  yield* Effect.sleep("2 seconds")
  console.log("done")
  return "some result"
})

Effect.runPromiseExit(program).then(console.log)
/*
输出:
start
done
{ _id: 'Exit', _tag: 'Success', value: 'some result' }
*/

示例(带中断)

在打印 "start" 后、"done" 前中断纤程。Effect.interrupt 会停止纤程,使其无法执行最终的日志打印。

import { Effect } from "effect"

const program = Effect.gen(function* () {
  console.log("start")
  yield* Effect.sleep("2 seconds")
  yield* Effect.interrupt
  console.log("done")
  return "some result"
})

Effect.runPromiseExit(program).then(console.log)
/*
输出:
start
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Interrupt',
    fiberId: {
      _id: 'FiberId',
      _tag: 'Runtime',
      id: 0,
      startTimeMillis: ...
    }
  }
}
*/

onInterrupt 方法

注册一个清理副作用,当主副作用被中断时执行。

该函数允许指定纤程中断时要运行的副作用,可用于执行清理操作或其他必要处理。

示例(中断时执行清理操作)

本示例设置了一个处理器,当纤程被中断时打印 “Cleanup completed”。通过三个场景(成功的副作用、失败的副作用、被中断的副作用),演示处理器在不同结束方式下的触发情况。

import { Console, Effect } from "effect"

// 纤程中断时执行的处理器
const handler = Effect.onInterrupt((_fibers) =>
  Console.log("Cleanup completed")
)

const success = Console.log("Task completed").pipe(
  Effect.as("some result"),
  handler
)

Effect.runFork(success)
/*
输出:
Task completed
*/

const failure = Console.log("Task failed").pipe(
  Effect.andThen(Effect.fail("some error")),
  handler
)

Effect.runFork(failure)
/*
输出:
Task failed
*/

const interruption = Console.log("Task interrupted").pipe(
  Effect.andThen(Effect.interrupt),
  handler
)

Effect.runFork(interruption)
/*
输出:
Task interrupted
Cleanup completed
*/

并发副作用的中断

当使用 Effect.forEach 等方式并发运行多个副作用时,若其中一个副作用被中断,所有并发副作用都会被中断。

最终的异常原因(cause)会包含所有被中断纤程的相关信息。

示例(中断并发副作用)

import { Effect, Console } from "effect"

const program = Effect.forEach(
  [1, 2, 3],
  (n) =>
    Effect.gen(function* () {
      console.log(`start #${n}`)
      yield* Effect.sleep(`${n} seconds`)
      if (n > 1) {
        yield* Effect.interrupt
      }
      console.log(`done #${n}`)
    }).pipe(Effect.onInterrupt(() => Console.log(`interrupted #${n}`))),
  { concurrency: "unbounded" }
)

Effect.runPromiseExit(program).then((exit) =>
  console.log(JSON.stringify(exit, null, 2))
)
/*
输出:
start #1
start #2
start #3
done #1
interrupted #2
interrupted #3
{
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Parallel",
    "left": {
      "_id": "Cause",
      "_tag": "Interrupt",
      "fiberId": {
        "_id": "FiberId",
        "_tag": "Runtime",
        "id": 3,
        "startTimeMillis": ...
      }
    },
    "right": {
      "_id": "Cause",
      "_tag": "Sequential",
      "left": {
        "_id": "Cause",
        "_tag": "Empty"
      },
      "right": {
        "_id": "Cause",
        "_tag": "Interrupt",
        "fiberId": {
          "_id": "FiberId",
          "_tag": "Runtime",
          "id": 0,
          "startTimeMillis": ...
        }
      }
    }
  }
}
*/

竞争机制

race 方法

接收两个副作用并并发运行。最先成功完成的副作用将决定竞争结果,另一个副作用会被中断。

若两个副作用均未成功,函数将失败并返回包含所有错误的异常原因(cause)。

适用于需并发运行两个副作用,但仅关注最先成功结果的场景,常见于超时处理、重试逻辑或优先获取更快响应的场景。

示例(两个任务均成功)

import { Effect, Console } from "effect"

const task1 = Effect.succeed("task1").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() => Console.log("task1 interrupted"))
)
const task2 = Effect.succeed("task2").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() => Console.log("task2 interrupted"))
)

const program = Effect.race(task1, task2)

Effect.runFork(program)
/*
输出:
task2 done
task1 interrupted
*/

示例(一个任务失败,一个任务成功)

import { Effect, Console } from "effect"

const task1 = Effect.fail("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() => Console.log("task1 interrupted"))
)
const task2 = Effect.succeed("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() => Console.log("task2 interrupted"))
)

const program = Effect.race(task1, task2)

Effect.runFork(program)
/*
输出:
task2 done
*/

示例(两个任务均失败)

import { Effect, Console } from "effect"

const task1 = Effect.fail("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() => Console.log("task1 interrupted"))
)
const task2 = Effect.fail("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() => Console.log("task2 interrupted"))
)

const program = Effect.race(task1, task2)

Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Parallel',
    left: { _id: 'Cause', _tag: 'Fail', failure: 'task1' },
    right: { _id: 'Cause', _tag: 'Fail', failure: 'task2' }
  }
}
*/

若需处理最先完成的任务结果(无论成功或失败),可使用 Effect.either 函数。该函数将结果包装为 Either 类型,可通过 Right(成功)或 Left(失败)标识结果状态:

示例(使用 Either 处理成功或失败)

import { Effect, Console } from "effect"

const task1 = Effect.fail("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() => Console.log("task1 interrupted"))
)
const task2 = Effect.succeed("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() => Console.log("task2 interrupted"))
)

// 并发运行两个任务,将结果包装为 Either 类型以捕获成功或失败状态
const program = Effect.race(Effect.either(task1), Effect.either(task2))

Effect.runPromise(program).then(console.log)
/*
输出:
task2 interrupted
{ _id: 'Either', _tag: 'Left', left: 'task1' }
*/

raceAll 方法

并发运行多个副作用,返回最先成功的结果。若某个副作用成功,其他副作用将被中断。

若所有副作用均未成功,函数将失败并返回最后遇到的错误。

适用于需竞争多个副作用,仅关注最先成功结果的场景,常见于超时处理、重试逻辑或优先获取更快响应的场景。

示例(所有任务均成功)

import { Effect, Console } from "effect"

const task1 = Effect.succeed("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() => Console.log("task1 interrupted"))
)
const task2 = Effect.succeed("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() => Console.log("task2 interrupted"))
)

const task3 = Effect.succeed("task3").pipe(
  Effect.delay("150 millis"),
  Effect.tap(Console.log("task3 done")),
  Effect.onInterrupt(() => Console.log("task3 interrupted"))
)

const program = Effect.raceAll([task1, task2, task3])

Effect.runFork(program)
/*
输出:
task1 done
task2 interrupted
task3 interrupted
*/

示例(一个任务失败,两个任务成功)

import { Effect, Console } from "effect"

const task1 = Effect.fail("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() => Console.log("task1 interrupted"))
)
const task2 = Effect.succeed("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() => Console.log("task2 interrupted"))
)

const task3 = Effect.succeed("task3").pipe(
  Effect.delay("150 millis"),
  Effect.tap(Console.log("task3 done")),
  Effect.onInterrupt(() => Console.log("task3 interrupted"))
)

const program = Effect.raceAll([task1, task2, task3])

Effect.runFork(program)
/*
输出:
task3 done
task2 interrupted
*/

示例(所有任务均失败)

import { Effect, Console } from "effect"

const task1 = Effect.fail("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() => Console.log("task1 interrupted"))
)
const task2 = Effect.fail("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() => Console.log("task2 interrupted"))
)

const task3 = Effect.fail("task3").pipe(
  Effect.delay("150 millis"),
  Effect.tap(Console.log("task3 done")),
  Effect.onInterrupt(() => Console.log("task3 interrupted"))
)

const program = Effect.raceAll([task1, task2, task3])

Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: { _id: 'Cause', _tag: 'Fail', failure: 'task2' }
}
*/

raceFirst 方法

接收两个副作用并并发运行,返回最先完成的结果(无论成功或失败)。

适用于需竞争两个操作,且希望以最先完成的结果继续执行的场景,无需关注结果状态。

示例(两个任务均成功)

import { Effect, Console } from "effect"

const task1 = Effect.succeed("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() =>
    Console.log("task1 interrupted").pipe(Effect.delay("100 millis"))
  )
)
const task2 = Effect.succeed("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() =>
    Console.log("task2 interrupted").pipe(Effect.delay("100 millis"))
  )
)

const program = Effect.raceFirst(task1, task2).pipe(
  Effect.tap(Console.log("more work..."))
)

Effect.runPromiseExit(program).then(console.log)
/*
输出:
task1 done
task2 interrupted
more work...
{ _id: 'Exit', _tag: 'Success', value: 'task1' }
*/

示例(一个任务失败,一个任务成功)

import { Effect, Console } from "effect"

const task1 = Effect.fail("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() =>
    Console.log("task1 interrupted").pipe(Effect.delay("100 millis"))
  )
)
const task2 = Effect.succeed("task2").pipe(
  Effect.delay("200 millis"),
  Effect.tap(Console.log("task2 done")),
  Effect.onInterrupt(() =>
    Console.log("task2 interrupted").pipe(Effect.delay("100 millis"))
  )
)

const program = Effect.raceFirst(task1, task2).pipe(
  Effect.tap(Console.log("more work..."))
)

Effect.runPromiseExit(program).then(console.log)
/*
输出:
task2 interrupted
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: { _id: 'Cause', _tag: 'Fail', failure: 'task1' }
}
*/

断开副作用连接

Effect.raceFirst 函数会在一个副作用完成后安全中断 “失败” 的副作用,但需等待失败的副作用完全终止后才会返回结果。

若需更快返回,可断开两个副作用的中断信号。无需调用:

Effect.raceFirst(task1, task2)

而是使用:

Effect.raceFirst(Effect.disconnect(task1), Effect.disconnect(task2))

这允许两个副作用独立完成,同时在后台终止失败的副作用。

示例(使用 Effect.disconnect 实现快速返回)

import { Effect, Console } from "effect"

const task1 = Effect.succeed("task1").pipe(
  Effect.delay("100 millis"),
  Effect.tap(Console.log("task1 done")),
  Effect.onInterrupt(() =>
    Console.log("task1 interrupted").pipe(Effect.delay("100 millis"))
  )
)

纤程(Fibers)

Effect 是一个由纤程(fibers)驱动的高并发框架。纤程是具备资源安全取消能力的轻量级虚拟线程,为 Effect 提供了诸多核心特性。

本节将带你了解纤程的基础知识,并熟悉一些基于纤程的强大底层操作符。

什么是虚拟线程?

JavaScript 本质上是单线程的,意味着代码会按单一指令序列执行。但现代 JavaScript 环境通过事件循环管理异步操作,营造出多任务执行的假象。

在这种背景下,虚拟线程(或纤程)是由 Effect 运行时模拟的逻辑线程。它们无需依赖 JavaScript 原生不支持的真正多线程,就能实现并发执行。

纤程的工作原理

Effect 中的所有副作用都通过纤程执行。若未手动创建纤程,则由正在使用的操作(若为并发操作)或 Effect 运行时系统自动创建。

每次运行副作用时都会创建一个纤程。并发运行副作用时,每个并发副作用都会对应一个纤程。

即便编写的是无并发操作的 “单线程” 代码,也至少会存在一个纤程 —— 即执行副作用的 “主纤程”。

Effect 纤程的生命周期与它所执行的副作用紧密相关。每个纤程最终都会以成功或失败的状态退出,具体取决于其执行的副作用是成功完成还是发生失败。

Effect 纤程拥有唯一标识、本地状态和运行状态(如已完成、运行中、挂起)。

核心概念总结

  • Effect:高层概念,描述带有副作用的计算。具有惰性和不可变性,代表可能产生值或失败但不会立即执行的计算。
  • 纤程(Fiber):代表 Effect 的运行实例。可被中断或等待以获取结果,是控制和交互正在进行的计算的方式。

纤程数据类型

Effect 中的 Fiber 数据类型代表对副作用执行过程的 “句柄”。

Fiber 的通用类型格式如下:

┌─── 代表成功结果类型
│        ┌─── 代表错误类型
│        │
▼        ▼
Fiber<Success, Error>

该类型表明纤程具备以下特性:

  • 成功时返回 Success 类型的值
  • 失败时抛出 Error 类型的错误

纤程没有 Requirements 类型参数,因为它们仅执行已满足依赖需求的副作用。

创建纤程(Forking Effects)

通过 “分叉”(fork)一个副作用,可以创建一个新的纤程。这会在新纤程中启动该副作用,并返回对该纤程的引用。

示例(创建纤程)

本示例中,斐波那契数列计算被分叉到独立纤程中,使其能脱离主纤程独立运行。后续可通过 fib10Fiber 引用对该纤程执行合并(join)或中断操作。

import { Effect } from "effect"

const fib = (n: number): Effect.Effect<number> =>
  n < 2
    ? Effect.succeed(n)
    : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b)

//      ┌─── Effect<RuntimeFiber<number, never>, never, never>
//      ▼
const fib10Fiber = Effect.fork(fib(10))

合并纤程(Joining Fibers)

纤程的常见操作之一是 “合并”(join)。使用 Fiber.join 函数可以等待目标纤程完成,并获取其执行结果。被合并的纤程无论成功或失败,join 返回的 Effect 都会反映该结果。

示例(合并纤程)

import { Effect, Fiber } from "effect"

const fib = (n: number): Effect.Effect<number> =>
  n < 2
    ? Effect.succeed(n)
    : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b)

//      ┌─── Effect<RuntimeFiber<number, never>, never, never>
//      ▼
const fib10Fiber = Effect.fork(fib(10))

const program = Effect.gen(function* () {
  // 获取纤程引用
  const fiber = yield* fib10Fiber
  // 合并纤程并获取结果
  const n = yield* Fiber.join(fiber)
  console.log(n)
})

Effect.runFork(program) // 输出:55

等待纤程(Awaiting Fibers)

Fiber.await 是操作纤程的实用工具。它允许等待纤程完成,并获取其终止的详细信息。结果封装在 Exit 类型中,可通过该类型判断纤程是成功、失败还是被中断。

示例(等待纤程完成)

import { Effect, Fiber } from "effect"

const fib = (n: number): Effect.Effect<number> =>
  n < 2
    ? Effect.succeed(n)
    : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b)

//      ┌─── Effect<RuntimeFiber<number, never>, never, never>
//      ▼
const fib10Fiber = Effect.fork(fib(10))

const program = Effect.gen(function* () {
  // 获取纤程引用
  const fiber = yield* fib10Fiber
  // 等待纤程完成并获取 Exit 结果
  const exit = yield* Fiber.await(fiber)
  console.log(exit)
})

Effect.runFork(program)
/*
输出:
{ _id: 'Exit', _tag: 'Success', value: 55 }
*/

中断模型(Interruption Model)

开发并发应用时,存在多种需要中断其他纤程的场景,例如:

  • 父纤程启动子纤程执行任务后,可能不再需要部分或全部子纤程的结果。
  • 多个纤程竞争执行,最先得出结果的纤程获胜,其他纤程无需继续执行,应被中断。
  • 交互式应用中,用户可能希望停止正在运行的任务(如点击 “停止” 按钮取消文件下载)。
  • 执行时间超出预期的计算,需通过超时操作中止。
  • 基于用户输入执行计算密集型任务时,若用户修改输入,应取消当前任务并执行新任务。

轮询式中断 vs 异步中断

中断纤程时,简单粗暴的方式是让一个纤程强制终止另一个纤程。但这种方式存在缺陷:若目标纤程正处于修改共享状态的过程中,可能导致共享状态不一致、不可靠,无法保证共享可变状态的内部一致性。

针对该问题,有两种主流且可靠的解决方案:

半异步中断(轮询式中断)

命令式语言(如 Java)常采用轮询作为半异步信号机制。该模型中,一个纤程向另一个纤程发送中断请求,目标纤程持续轮询中断状态,检测是否收到其他纤程的中断请求。若检测到请求,目标纤程会尽快自行终止。

这种方案中,纤程自行处理临界区逻辑。若纤程处于临界区时收到中断请求,会忽略该请求,待临界区执行完毕后再处理。

但该方案存在缺点:若开发者忘记定期轮询,目标纤程可能变得无响应,进而导致死锁。此外,轮询全局标志与 Effect 遵循的函数式范式不兼容。

异步中断

异步中断允许一个纤程终止另一个纤程。目标纤程无需负责轮询中断状态,而是在临界区期间禁用该区域的可中断性。这是一种纯函数式解决方案,无需轮询全局状态。

Effect 的中断模型采用异步中断,这是一种完全异步的信号机制。该机制克服了忘记定期轮询的缺陷,且与函数式范式完全兼容 —— 在纯函数式计算中,除了禁用中断的临界区外,可在任意时刻中止计算。

中断纤程(Interrupting Fibers)

若纤程的结果不再需要,可对其执行中断操作。该操作会立即停止纤程,并安全执行所有终结器以释放资源。

与 Fiber.await 类似,Fiber.interrupt 函数返回 Exit 类型的值,包含纤程终止的详细信息。

示例(中断纤程)

import { Effect, Fiber } from "effect"

const program = Effect.gen(function* () {
  // 分叉一个无限循环的纤程,每 10 毫秒打印 "Hi!"
  const fiber = yield* Effect.fork(
    Effect.forever(Effect.log("Hi!").pipe(Effect.delay("10 millis")))
  )
  yield* Effect.sleep("30 millis")
  // 中断纤程并获取其终止的详细 Exit 信息
  const exit = yield* Fiber.interrupt(fiber)
  console.log(exit)
})

Effect.runFork(program)
/*
输出:
timestamp=... level=INFO fiber=#1 message=Hi!
timestamp=... level=INFO fiber=#1 message=Hi!
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Interrupt',
    fiberId: {
      _id: 'FiberId',
      _tag: 'Runtime',
      id: 0,
      startTimeMillis: ...
    }
  }
}
*/

默认情况下,Fiber.interrupt 返回的副作用会等待纤程完全终止后再恢复执行。这确保了新纤程不会在之前的纤程结束前启动,该行为称为 “背压(back-pressuring)”。

若无需等待,可将中断操作本身分叉,让主程序无需等待纤程终止即可继续执行:

示例(分叉中断操作)

import { Effect, Fiber } from "effect"

const program = Effect.gen(function* () {
  const fiber = yield* Effect.fork(
    Effect.forever(Effect.log("Hi!").pipe(Effect.delay("10 millis")))
  )
  yield* Effect.sleep("30 millis")
  const _ = yield* Effect.fork(Fiber.interrupt(fiber))
  console.log("Do something else...")
})

Effect.runFork(program)
/*
输出:
timestamp=... level=INFO fiber=#1 message=Hi!
timestamp=... level=INFO fiber=#1 message=Hi!
Do something else...
*/

Effect 还提供了后台中断的简写方法 Fiber.interruptFork

import { Effect, Fiber } from "effect"

const program = Effect.gen(function* () {
  const fiber = yield* Effect.fork(
    Effect.forever(Effect.log("Hi!").pipe(Effect.delay("10 millis")))
  )
  yield* Effect.sleep("30 millis")
  // const _ = yield* Effect.fork(Fiber.interrupt(fiber))
  const _ = yield* Fiber.interruptFork(fiber)
  console.log("Do something else...")
})

Effect.runFork(program)
/*
输出:
timestamp=... level=INFO fiber=#1 message=Hi!
timestamp=... level=INFO fiber=#1 message=Hi!
Do something else...
*/

组合纤程(Composing Fibers)

Fiber.zip 和 Fiber.zipWith 函数允许将两个纤程组合为一个。组合后的纤程会生成两个输入纤程的结果,若任一输入纤程失败,组合纤程也会失败。

示例(使用 Fiber.zip 组合纤程)

本示例中,两个纤程并发运行,结果将组合为一个元组返回。

import { Effect, Fiber } from "effect"

const program = Effect.gen(function* () {
  // 分叉两个分别返回字符串的纤程
  const fiber1 = yield* Effect.fork(Effect.succeed("Hi!"))
  const fiber2 = yield* Effect.fork(Effect.succeed("Bye!"))

  // 使用 Fiber.zip 组合两个纤程
  const fiber = Fiber.zip(fiber1, fiber2)

  // 合并组合后的纤程,获取元组形式的结果
  const tuple = yield* Fiber.join(fiber)
  console.log(tuple)
})

Effect.runFork(program)
/*
输出:
[ 'Hi!', 'Bye!' ]
*/

另一种组合纤程的方式是使用 Fiber.orElse。该函数允许指定一个备用纤程:若第一个纤程失败,则执行备用纤程;若第一个纤程成功,则返回其结果;若第一个纤程失败,无论备用纤程结果如何,都返回备用纤程的结果。

示例(使用 Fiber.orElse 设置备用纤程)

import { Effect, Fiber } from "effect"

const program = Effect.gen(function* () {
  // 分叉一个会失败的纤程
  const fiber1 = yield* Effect.fork(Effect.fail("Uh oh!"))
  // 分叉一个会成功的纤程
  const fiber2 = yield* Effect.fork(Effect.succeed("Hurray!"))
  // 若 fiber1 失败,则使用 fiber2 作为备用
  const fiber = Fiber.orElse(fiber1, fiber2)
  const message = yield* Fiber.join(fiber)
  console.log(message)
})

Effect.runFork(program)
/*
输出:
Hurray!
*/

子纤程的生命周期(Lifetime of Child Fibers)

分叉纤程时,根据分叉方式的不同,子纤程有四种不同的生命周期策略:

自动监控式分叉(Fork With Automatic Supervision)

使用普通的 Effect.fork 操作时,子纤程会由父纤程自动监控。子纤程的生命周期与父纤程绑定,意味着子纤程会在自然结束或父纤程终止时被终止。

示例(自动监控的子纤程)

本场景中,parent 纤程启动 child 纤程,child 纤程每秒重复打印一条消息。child 纤程会在 parent 纤程完成时被终止。

import { Effect, Console, Schedule } from "effect"

// 子纤程:每秒重复打印消息
const child = Effect.repeat(
  Console.log("child: still running!"),
  Schedule.fixed("1 second")
)

const parent = Effect.gen(function* () {
  console.log("parent: started!")
  // 子纤程由父纤程监控
  yield* Effect.fork(child)
  yield* Effect.sleep("3 seconds")
  console.log("parent: finished!")
})

Effect.runFork(parent)
/*
输出:
parent: started!
child: still running!
child: still running!
child: still running!
parent: finished!
*/

该行为可扩展到任意嵌套层级的纤程,确保纤程生命周期可预测、可控制。

全局作用域分叉(守护进程,Fork in Global Scope (Daemon))

有时需要运行与父纤程生命周期解绑的长期后台纤程,并将其分叉到全局作用域。在全局作用域分叉的纤程会成为守护纤程(daemon fiber),可通过 Effect.forkDaemon 操作符实现。

守护纤程没有父纤程,不受监控,会在自然结束或应用终止时被终止。

示例(创建守护纤程)

本示例展示了守护纤程在父纤程完成后,仍能在后台继续运行。

import { Effect, Console, Schedule } from "effect"

// 守护纤程:每秒重复打印消息
const daemon = Effect.repeat(
  Console.log("daemon: still running!"),
  Schedule.fixed("1 second")
)

const parent = Effect.gen(function* () {
  console.log("parent: started!")
  // 守护纤程独立运行
  yield* Effect.forkDaemon(daemon)
  yield* Effect.sleep("3 seconds")
  console.log("parent: finished!")
})

Effect.runFork(parent)
/*
输出:
parent: started!
daemon: still running!
daemon: still running!
daemon: still running!
parent: finished!
daemon: still running!
daemon: still running!
daemon: still running!
daemon: still running!
daemon: still running!
...以此类推...
*/

即使父纤程被中断,守护纤程仍会独立继续运行。

示例(中断父纤程)

本示例中,中断父纤程不会影响守护纤程,守护纤程会继续在后台运行。

import { Effect, Console, Schedule, Fiber } from "effect"

// 守护纤程:每秒重复打印消息
const daemon = Effect.repeat(
  Console.log("daemon: still running!"),
  Schedule.fixed("1 second")
)

const parent = Effect.gen(function* () {
  console.log("parent: started!")
  // 守护纤程独立运行
  yield* Effect.forkDaemon(daemon)
  yield* Effect.sleep("3 seconds")
  console.log("parent: finished!")
}).pipe(Effect.onInterrupt(() => Console.log("parent: interrupted!")))

// 2 秒后中断父纤程的程序
const program = Effect.gen(function* () {
  const fiber = yield* Effect.fork(parent)
  yield* Effect.sleep("2 seconds")
  yield* Fiber.interrupt(fiber) // 中断父纤程
})

Effect.runFork(program)
/*
输出:
parent: started!
daemon: still running!
daemon: still running!
parent: interrupted!
daemon: still running!
daemon: still running!
daemon: still running!
daemon: still running!
daemon: still running!
...以此类推...
*/

局部作用域分叉(Fork in Local Scope)

有时需要创建与父纤程生命周期解绑,但绑定到局部作用域的纤程。可通过 Effect.forkScoped 操作符在局部作用域分叉纤程。

使用 Effect.forkScoped 创建的纤程可以存活到父纤程终止之后,仅在局部作用域关闭时被终止。

示例(在局部作用域分叉纤程)

本示例中,child 纤程的生命周期超出 parent 纤程,仅在局部作用域结束时被终止。

import { Effect, Console, Schedule } from "effect"

// 子纤程:每秒重复打印消息
const child = Effect.repeat(
  Console.log("child: still running!"),
  Schedule.fixed("1 second")
)

//      ┌─── Effect<void, never, Scope>
//      ▼
const parent = Effect.gen(function* () {
  console.log("parent: started!")
  // 子纤程绑定到局部作用域
  yield* Effect.forkScoped(child)
  yield* Effect.sleep("3 seconds")
  console.log("parent: finished!")
})

// 在局部作用域中运行程序
const program = Effect.scoped(
  Effect.gen(function* () {
    console.log("Local scope started!")
    yield* Effect.fork(parent)
    // 作用域持续 5 秒
    yield* Effect.sleep("5 seconds")
    console.log("Leaving the local scope!")
  })
)

Effect.runFork(program)
/*
输出:
Local scope started!
parent: started!
child: still running!
child: still running!
child: still running!
parent: finished!
child: still running!
child: still running!
Leaving the local scope!
*/

指定作用域分叉(Fork in Specific Scope)

某些场景下需要更精细的控制,可将纤程分叉到指定作用域中。通过 Effect.forkIn 操作符实现,该操作符接收目标作用域作为参数。

示例(在指定作用域分叉纤程)

本示例中,child 纤程被分叉到 outerScope 中,使其能存活到内层作用域终止之后,但仍会在 outerScope 关闭时被终止。

import { Console, Effect, Schedule } from "effect"

// 子纤程:每秒重复打印消息
const child = Effect.repeat(
  Console.log("child: still running!"),
  Schedule.fixed("1 second")
)

const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() =>
      Console.log("The outer scope is about to be closed!")
    )

    // 获取外层作用域
    const outerScope = yield* Effect.scope

    // 创建内层作用域
    yield* Effect.scoped(
      Effect.gen(function* () {
        yield* Effect.addFinalizer(() =>
          Console.log("The inner scope is about to be closed!")
        )
        // 在外部作用域中分叉子纤程
        yield* Effect.forkIn(child, outerScope)
        yield* Effect.sleep("3 seconds")
      })
    )

    yield* Effect.sleep("5 seconds")
  })
)

Effect.runFork(program)
/*
输出:
child: still running!
child: still running!
child: still running!
The inner scope is about to be closed!
child: still running!
child: still running!
child: still running!
child: still running!
child: still running!
child: still running!
The outer scope is about to be closed!
*/

纤程何时运行?(When do Fibers run?)

分叉的纤程会在当前纤程完成或让步(yield)后开始执行。

示例(纤程延迟启动仅捕获一个值)

以下示例中,changes 流仅捕获到值 2。这是因为 Effect.fork 创建的纤程在值更新后才启动。

import { Effect, SubscriptionRef, Stream, Console } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* SubscriptionRef.make(0)
  yield* ref.changes.pipe(
    // 记录 SubscriptionRef 的每次变更
    Stream.tap((n) => Console.log(`SubscriptionRef changed to ${n}`)),
    Stream.runDrain,
    // 分叉纤程运行流
    Effect.fork
  )
  yield* SubscriptionRef.set(ref, 1)
  yield* SubscriptionRef.set(ref, 2)
})

Effect.runFork(program)
/*
输出:
SubscriptionRef changed to 2
*/

若通过 Effect.sleep() 添加短暂延迟或调用 Effect.yieldNow(),可让当前纤程让步,使分叉的纤程有足够时间启动,从而在值更新前捕获所有变更。

纤程执行具有不确定性(Fiber Execution is Non-Deterministic)

需注意,纤程执行的时机具有不确定性,多种因素会影响纤程的启动时间。不要依赖 “单次让步必然让纤程在特定时间启动” 的假设。

示例(延迟让纤程捕获所有值)

import { Effect, SubscriptionRef, Stream, Console } from "effect"

const program = Effect.gen(function* () {
  const ref = yield* SubscriptionRef.make(0)
  yield* ref.changes.pipe(
    // 记录 SubscriptionRef 的每次变更
    Stream.tap((n) => Console.log(`SubscriptionRef changed to ${n}`)),
    Stream.runDrain,
    // 分叉纤程运行流
    Effect.fork
  )

  // 给纤程启动的时间
  yield* Effect.sleep("100 millis")

  yield* SubscriptionRef.set(ref, 1)
  yield* SubscriptionRef.set(ref, 2)
})

Effect.runFork(program)
/*
输出:
SubscriptionRef changed to 1
SubscriptionRef changed to 2
*/

延迟对象(Deferred)

概述(Overview)

Deferred<Success, Error> 是 Effect 的专用子类型,类似一个具有独特特性的一次性变量。它只能被完成一次,是管理异步操作和程序不同部分间同步的实用工具。

延迟对象本质上是一种同步原语,代表一个可能暂不可用的值。创建延迟对象时它处于空状态,后续可通过成功值 Success 或错误值 Error 完成初始化:

plaintext

┌─── 代表成功结果类型
│        ┌─── 代表错误类型
│        │
▼        ▼
Deferred<Success, Error>

一旦完成初始化,其状态便不可再更改。

当纤程调用 Deferred.await 时,会暂停执行直到延迟对象完成初始化。等待期间,纤程不会阻塞线程,仅进行语义上的阻塞,确保其他纤程可正常运行,实现高效并发。

延迟对象在概念上与 JavaScript 的 Promise 类似,核心区别在于它同时支持成功类型和错误类型,提供更强的类型安全性。

创建延迟对象(Creating a Deferred)

可通过 Deferred.make 构造函数创建延迟对象,该函数返回一个表示延迟对象创建过程的副作用。由于创建延迟对象涉及内存分配,必须在副作用中执行以确保资源的安全管理。

示例(创建延迟对象)

import { Deferred } from "effect"

//      ┌─── Effect<Deferred<string, Error>>
//      ▼
const deferred = Deferred.make<string, Error>()

等待延迟对象(Awaiting)

要从延迟对象中获取值,可使用 Deferred.await 方法。该操作会暂停调用纤程,直到延迟对象被赋予值或错误。

import { Effect, Deferred } from "effect"

//      ┌─── Effect<Deferred<string, Error>, never, never>
//      ▼
const deferred = Deferred.make<string, Error>()

//      ┌─── Effect<string, Error, never>
//      ▼
const value = deferred.pipe(Effect.andThen(Deferred.await))

完成延迟对象(Completing)

根据需求(成功、失败或中断等待中的纤程),可通过多种方式完成延迟对象的初始化:

API描述
Deferred.succeed以成功状态完成延迟对象,赋予其一个值
Deferred.done用 Exit 值完成延迟对象的初始化
Deferred.complete用某个副作用的结果完成延迟对象的初始化
Deferred.completeWith用一个副作用完成延迟对象的初始化(该副作用会被每个等待中的纤程执行,需谨慎使用)
Deferred.fail以失败状态完成延迟对象,赋予其一个错误
Deferred.die用用户自定义错误使延迟对象出现故障
Deferred.failCause用 Cause 对象使延迟对象失败或出现故障
Deferred.interrupt中断延迟对象,强制停止或中断所有等待中的纤程

示例(以成功状态完成延迟对象)

import { Effect, Deferred } from "effect"

const program = Effect.gen(function* () {
  const deferred = yield* Deferred.make<number, string>()

  // 以成功状态完成延迟对象的初始化
  yield* Deferred.succeed(deferred, 1)

  // 等待延迟对象并获取其值
  const value = yield* Deferred.await(deferred)

  console.log(value)
})

Effect.runFork(program)
// 输出:1

完成延迟对象的操作会返回一个 Effect<boolean>。若延迟对象成功完成初始化,该副作用返回 true;若已完成过初始化,则返回 false,可用于跟踪延迟对象的状态。

示例(检查完成状态)

import { Effect, Deferred } from "effect"

const program = Effect.gen(function* () {
  const deferred = yield* Deferred.make<number, string>()

  // 尝试以失败状态完成延迟对象
  const firstAttempt = yield* Deferred.fail(deferred, "oh no!")

  // 已完成初始化后,尝试以成功状态重新赋值
  const secondAttempt = yield* Deferred.succeed(deferred, 1)

  console.log([firstAttempt, secondAttempt])
})

Effect.runFork(program)
// 输出:[ true, false ]

检查完成状态(Checking Completion Status)

有时可能需要在不暂停纤程的情况下检查延迟对象是否已完成初始化,可通过 Deferred.poll 方法实现:

Deferred.poll 返回 Option<Effect<A, E>>

  • 若延迟对象未完成初始化,返回 None
  • 若延迟对象已完成初始化,返回 Some,其中包含结果或错误

此外,还可使用 Deferred.isDone 函数检查延迟对象是否已完成初始化。该方法返回 Effect<boolean>,若延迟对象已完成则返回 true,方便快速查询状态。

示例(轮询与检查完成状态)

import { Effect, Deferred } from "effect"

const program = Effect.gen(function* () {
  const deferred = yield* Deferred.make<number, string>()

  // 轮询延迟对象,检查是否已完成初始化
  const done1 = yield* Deferred.poll(deferred)

  // 检查延迟对象是否已完成初始化
  const done2 = yield* Deferred.isDone(deferred)

  console.log([done1, done2])
})

Effect.runFork(program)
/*
输出:
[ { _id: 'Option', _tag: 'None' }, false ]
*/

常见使用场景(Common Use Cases)

当程序中需要等待特定事件发生时,Deferred 会发挥重要作用。它非常适合让代码的一部分在准备就绪时向另一部分发送信号的场景。

以下是一些常见使用场景:

使用场景描述
纤程协调(Coordinating Fibers)当存在多个并发任务且需要协调它们的操作时,Deferred 可帮助一个纤程向另一个纤程发送任务完成的信号
同步(Synchronization)任何需要确保一段代码在另一段代码完成工作后再执行的场景,Deferred 都能提供所需的同步机制
工作交接(Handing Over Work)可使用 Deferred 在纤程间交接工作,例如一个纤程准备好数据后,由第二个纤程继续处理
暂停执行(Suspending Execution)当需要让纤程暂停执行直到某个条件满足时,Deferred 可用于阻塞纤程,直到条件达成

示例(使用 Deferred 协调两个纤程)

本示例中,延迟对象用于在两个纤程间传递值。通过并发运行两个纤程并以延迟对象作为同步点,确保 fiberB 仅在 fiberA 完成任务后才继续执行。

import { Effect, Deferred, Fiber } from "effect"

const program = Effect.gen(function* () {
  const deferred = yield* Deferred.make<string, string>()

  // 延迟一段时间后,以值完成延迟对象的初始化
  const taskA = Effect.gen(function* () {
    console.log("Starting task to complete the Deferred")
    yield* Effect.sleep("1 second")
    console.log("Completing the Deferred")
    return yield* Deferred.succeed(deferred, "hello world")
  })

  // 等待延迟对象并打印获取到的值
  const taskB = Effect.gen(function* () {
    console.log("Starting task to get the value from the Deferred")
    const value = yield* Deferred.await(deferred)
    console.log("Got the value from the Deferred")
    return value
  })

  // 并发运行两个纤程
  const fiberA = yield* Effect.fork(taskA)
  const fiberB = yield* Effect.fork(taskB)

  // 等待两个纤程都完成执行
  const both = yield* Fiber.join(Fiber.zip(fiberA, fiberB))

  console.log(both)
})

Effect.runFork(program)
/*
输出:
Starting task to complete the Deferred
Starting task to get the value from the Deferred
Completing the Deferred
Got the value from the Deferred
[ true, 'hello world' ]
*/

队列(Queue)

Queue 是一种轻量级内存队列,内置背压机制,支持异步、纯函数式且类型安全的数据处理。

基本操作(Basic Operations)

Queue<A> 存储类型为 A 的值,并提供两个核心操作:

API描述
Queue.offer向队列中添加一个类型为 A 的值
Queue.take从队列中移除并返回最旧的值

示例(添加与获取元素)

import { Effect, Queue } from "effect"

const program = Effect.gen(function* () {
  // 创建一个容量为 100 的有界队列
  const queue = yield* Queue.bounded<number>(100)
  // 向队列中添加 1
  yield* Queue.offer(queue, 1)
  // 检索并移除最旧的值
  const value = yield* Queue.take(queue)
  return value
})

Effect.runPromise(program).then(console.log)
// 输出:1

创建队列(Creating a Queue)

队列分为有界队列(指定容量)和无界队列(无容量限制)。不同类型的队列在达到容量上限时,处理新值的方式不同。

有界队列(Bounded Queue)

有界队列在满容量时会施加背压,即任何 Queue.offer 操作都会暂停,直到队列有空闲空间。

示例(创建有界队列)
import { Queue } from "effect"

// 创建一个容量为 100 的有界队列
const boundedQueue = Queue.bounded<number>(100)

丢弃队列(Dropping Queue)

丢弃队列在满容量时会直接丢弃新值。

示例(创建丢弃队列)
import { Queue } from "effect"

// 创建一个容量为 100 的丢弃队列
const droppingQueue = Queue.dropping<number>(100)

滑动队列(Sliding Queue)

滑动队列在达到容量上限时,会移除旧值以腾出空间容纳新值。

示例(创建滑动队列)
import { Queue } from "effect"

// 创建一个容量为 100 的滑动队列
const slidingQueue = Queue.sliding<number>(100)

无界队列(Unbounded Queue)

无界队列没有容量限制,允许无限制地添加元素。

示例(创建无界队列)
import { Queue } from "effect"

// 创建一个无容量限制的无界队列
const unboundedQueue = Queue.unbounded<number>()

向队列添加元素(Adding Items to a Queue)

offer 方法

使用 Queue.offer 向队列添加单个值。

示例(添加单个元素)
import { Effect, Queue } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(100)
  // 向队列中添加 1
  yield* Queue.offer(queue, 1)
})

对于带背压的队列,若队列已满,Queue.offer 会暂停执行。为避免阻塞主纤程,可将 Queue.offer 操作分叉到新纤程中。

示例(使用 Effect.fork 处理满队列)
import { Effect, Queue, Fiber } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(1)
  // 向队列添加一个元素,填满队列
  yield* Queue.offer(queue, 1)
  // 队列已满,尝试添加第二个元素会暂停
  const fiber = yield* Effect.fork(Queue.offer(queue, 2))
  // 取出队列中的元素,腾出空间
  yield* Queue.take(queue)
  // 合并纤程,完成暂停的 offer 操作
  yield* Fiber.join(fiber)
  // 返回添加元素后的队列大小
  return yield* Queue.size(queue)
})

Effect.runPromise(program).then(console.log)
// 输出:1

offerAll 方法

使用 Queue.offerAll 可一次性向队列添加多个元素。

示例(添加多个元素)
import { Effect, Queue, Array } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(100)
  const items = Array.range(1, 10)
  // 一次性向队列添加所有元素
  yield* Queue.offerAll(queue, items)
  // 返回添加元素后的队列大小
  return yield* Queue.size(queue)
})

Effect.runPromise(program).then(console.log)
// 输出:10

从队列消费元素(Consuming Items from a Queue)

take 方法

Queue.take 操作会移除并返回队列中最旧的元素。若队列为空,Queue.take 会暂停,直到有新元素添加。为避免阻塞,可将 Queue.take 操作分叉到新纤程中。

示例(在纤程中等待元素)
import { Effect, Queue, Fiber } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<string>(100)
  // 队列为空,该 take 操作会暂停
  const fiber = yield* Effect.fork(Queue.take(queue))
  // 向队列添加一个元素
  yield* Queue.offer(queue, "something")
  // 合并纤程,获取 take 操作的结果
  const value = yield* Fiber.join(fiber)
  return value
})

Effect.runPromise(program).then(console.log)
// 输出:something

poll 方法

使用 Queue.poll 可在不暂停的情况下检索队列的首个元素。若队列为空,返回 None;若存在元素,返回包裹该元素的 Some

示例(轮询元素)
import { Effect, Queue } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(100)
  // 向队列添加元素
  yield* Queue.offer(queue, 10)
  yield* Queue.offer(queue, 20)
  // 检索首个可用元素
  const head = yield* Queue.poll(queue)
  return head
})

Effect.runPromise(program).then(console.log)
/*
输出:
{
  _id: "Option",
  _tag: "Some",
  value: 10
}
*/

takeUpTo 方法

使用 Queue.takeUpTo 可检索最多指定数量的元素。若元素不足,返回所有可用元素,不会等待更多元素。

该函数特别适用于不需要精确数量元素的批处理场景,确保程序能利用当前可用数据继续运行。若需等待精确数量的元素再执行,可使用 takeN 方法。

示例(最多获取 N 个元素)
import { Effect, Queue } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(100)

  // 向队列添加元素
  yield* Queue.offer(queue, 1)
  yield* Queue.offer(queue, 2)
  yield* Queue.offer(queue, 3)

  // 最多检索 2 个元素
  const chunk = yield* Queue.takeUpTo(queue, 2)
  console.log(chunk)

  return "some result"
})

Effect.runPromise(program).then(console.log)
/*
输出:
{ _id: 'Chunk', values: [ 1, 2 ] }
some result
*/

takeN 方法

从队列中获取指定数量的元素。若队列中元素不足,该操作会暂停,直到所需数量的元素全部可用。

该函数适用于每次处理需要精确数量元素的场景,确保批处理完成后再继续执行。

示例(获取固定数量的元素)
import { Effect, Queue, Fiber } from "effect"

const program = Effect.gen(function* () {
  // 创建一个最多可容纳 100 个元素的队列
  const queue = yield* Queue.bounded<number>(100)

  // 分叉一个纤程,尝试从队列中获取 3 个元素
  const fiber = yield* Effect.fork(
    Effect.gen(function* () {
      console.log("尝试从队列中获取 3 个元素...")
      const chunk = yield* Queue.takeN(queue, 3)
      console.log(`成功获取 3 个元素:${JSON.stringify(chunk)}`)
    })
  )

  // 初始仅添加 2 个元素
  yield* Queue.offer(queue, 1)
  yield* Queue.offer(queue, 2)
  console.log(
    "已添加 2 个元素。纤程正在等待第 3 个元素..."
  )

  // 模拟延迟
  yield* Effect.sleep("2 seconds")

  // 添加第 3 个元素,解除 takeN 的阻塞
  yield* Queue.offer(queue, 3)
  console.log("已添加第 3 个元素,纤程将解除阻塞。")

  // 等待纤程完成
  yield* Fiber.join(fiber)
  return "some result"
})

Effect.runPromise(program).then(console.log)
/*
输出:
已添加 2 个元素。纤程正在等待第 3 个元素...
尝试从队列中获取 3 个元素...
已添加第 3 个元素,纤程将解除阻塞。
成功获取 3 个元素:{"_id":"Chunk","values":[1,2,3]}
some result
*/

takeAll 方法

使用 Queue.takeAll 可一次性获取队列中的所有元素。该操作立即完成,若队列为空则返回空集合。

示例(获取所有元素)
import { Effect, Queue } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(100)
  // 向队列添加元素
  yield* Queue.offer(queue, 10)
  yield* Queue.offer(queue, 20)
  yield* Queue.offer(queue, 30)
  // 从队列中获取所有元素
  const chunk = yield* Queue.takeAll(queue)
  return chunk
})

Effect.runPromise(program).then(console.log)
/*
输出:
{
  _id: "Chunk",
  values: [ 10, 20, 30 ]
}
*/

关闭队列(Shutting Down a Queue)

shutdown 方法

Queue.shutdown 操作会中断所有因 offer* 或 take* 操作而暂停的纤程。该操作还会清空队列,并使后续所有 offer* 和 take* 调用立即终止。

示例(关闭队列时中断纤程)
import { Effect, Queue, Fiber } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(3)
  // 分叉一个纤程,等待从队列中获取元素
  const fiber = yield* Effect.fork(Queue.take(queue))
  // 关闭队列,中断纤程
  yield* Queue.shutdown(queue)
  // 合并被中断的纤程
  yield* Fiber.join(fiber)
})

awaitShutdown 方法

Queue.awaitShutdown 操作可用于在队列关闭时执行某个副作用。它会等待队列关闭,若队列已关闭则立即恢复执行。

示例(等待队列关闭)
import { Effect, Queue, Fiber, Console } from "effect"

const program = Effect.gen(function* () {
  const queue = yield* Queue.bounded<number>(3)
  // 分叉一个纤程,等待队列关闭并打印消息
  const fiber = yield* Effect.fork(
    Queue.awaitShutdown(queue).pipe(
      Effect.andThen(Console.log("shutting down"))
    )
  )
  // 关闭队列,触发纤程中的 await 操作
  yield* Queue.shutdown(queue)
  yield* Fiber.join(fiber)
})

Effect.runPromise(program)
// 输出:shutting down

仅入队 / 仅出队队列(Offer-only / Take-only Queues)

有时可能需要限制代码的某些部分只能向队列添加值(仅入队,Enqueue)或只能从队列获取值(仅出队,Dequeue)。Effect 提供了对应的接口来实现这些限制。

仅入队(Enqueue)

Enqueue 接口定义了所有向队列添加值的方法,限制队列仅支持入队操作。

示例(限制队列仅支持入队操作)
import { Queue } from "effect"

const send = (offerOnlyQueue: Queue.Enqueue<number>, value: number) => {
  // 该队列仅支持入队操作

  // 错误:不能在仅入队队列上使用 take 操作
  Queue.take(offerOnlyQueue)
  // Error ts(2345) — 类型 'Enqueue<number>' 不能分配给类型 'Dequeue<unknown>'。
  //   类型 'Enqueue<number>' 缺少类型 'Dequeue<unknown>' 的以下属性:take、takeAll、takeUpTo、takeBetween 等 6 个属性。

  // 合法的入队操作
  return Queue.offer(offerOnlyQueue, value)
}

仅出队(Dequeue)

同理,Dequeue 接口定义了所有从队列获取值的方法,限制队列仅支持出队操作。

示例(限制队列仅支持出队操作)
import { Queue } from "effect"

const receive = (takeOnlyQueue: Queue.Dequeue<number>) => {
  // 该队列仅支持出队操作

  // 错误:不能在仅出队队列上使用 offer 操作
  Queue.offer(takeOnlyQueue, 1)
  // Error ts(2345) — 类型 'Dequeue<number>' 不能分配给类型 'Enqueue<number>'。
  //   类型 'Dequeue<number>' 缺少类型 'Enqueue<number>' 的以下属性:offer、unsafeOffer、offerAll、[EnqueueTypeId]

  // 合法的出队操作
  return Queue.take(takeOnlyQueue)
}

Queue 类型同时集成了 Enqueue 和 Dequeue 接口,因此可根据需要将其传递给代码的不同部分,分别限制为仅入队或仅出队行为。

示例(结合使用仅入队和仅出队队列)
import { Effect, Queue } from "effect"

const send = (offerOnlyQueue: Queue.Enqueue<number>, value: number) => {
  return Queue.offer(offerOnlyQueue, value)
}

const receive = (takeOnlyQueue: Queue.Dequeue<number>) => {
  return Queue.take(takeOnlyQueue)
}

const program = Effect.gen(function* () {
  const queue = yield* Queue.unbounded<number>()

  // 向队列添加值
  yield* send(queue, 1)
  yield* send(queue, 2)

  // 从队列获取值
  console.log(yield* receive(queue))
  console.log(yield* receive(queue))
})

Effect.runFork(program)
/*
输出:
1
2
*/

发布 - 订阅(PubSub)

PubSub 是一个异步消息中心,允许发布者发送消息,所有当前订阅者均可接收这些消息。

与队列(Queue)不同(每个值仅传递给一个消费者),PubSub 会将每条已发布的消息广播给所有订阅者。这使得 PubSub 非常适合需要消息广播而非负载分发的场景。

基本操作(Basic Operations)

PubSub<A> 存储类型为 A 的消息,并提供两个核心操作:

API描述
PubSub.publish向 PubSub 发送一条类型为 A 的消息,返回一个表示消息是否成功发布的副作用
PubSub.subscribe创建一个作用域内的副作用,用于订阅 PubSub,作用域结束时会自动取消订阅。订阅者通过存储已发布消息的 Dequeue 接收消息

示例(向多个订阅者发布消息)

import { Effect, PubSub, Queue } from "effect"

const program = Effect.scoped(
  Effect.gen(function* () {
    const pubsub = yield* PubSub.bounded<string>(2)

    // 两个订阅者
    const dequeue1 = yield* PubSub.subscribe(pubsub)
    const dequeue2 = yield* PubSub.subscribe(pubsub)

    // 向 PubSub 发布一条消息
    yield* PubSub.publish(pubsub, "Hello from a PubSub!")

    // 每个订阅者都能收到消息
    console.log("Subscriber 1: " + (yield* Queue.take(dequeue1)))
    console.log("Subscriber 2: " + (yield* Queue.take(dequeue2)))
  })
)

Effect.runFork(program)
/*
输出:
Subscriber 1: Hello from a PubSub!
Subscriber 2: Hello from a PubSub!
*/

创建 PubSub(Creating a PubSub)

有界 PubSub(Bounded PubSub)

有界 PubSub 达到容量上限时会向发布者施加背压,暂停后续发布操作,直到有空闲空间可用。

背压机制确保所有订阅者在订阅期间都能收到所有消息,但如果某个订阅者处理速度较慢,可能会导致消息交付延迟。

示例(创建有界 PubSub)
import { PubSub } from "effect"

// 创建一个容量为 2 的有界 PubSub
const boundedPubSub = PubSub.bounded<string>(2)

丢弃 PubSub(Dropping PubSub)

丢弃 PubSub 满容量时会直接丢弃新消息。若消息被丢弃,PubSub.publish 操作会返回 false

在丢弃型 PubSub 中,发布者可以继续发布新值,但无法保证订阅者能收到所有消息。

示例(创建丢弃 PubSub)
import { PubSub } from "effect"

// 创建一个容量为 2 的丢弃 PubSub
const droppingPubSub = PubSub.dropping<string>(2)

滑动 PubSub(Sliding PubSub)

滑动 PubSub 达到容量上限时,会移除最旧的消息以腾出空间容纳新消息,确保发布操作永不阻塞。

滑动型 PubSub 可避免慢速订阅者影响消息交付速率,但慢速订阅者仍有可能错过部分消息。

示例(创建滑动 PubSub)
import { PubSub } from "effect"

// 创建一个容量为 2 的滑动 PubSub
const slidingPubSub = PubSub.sliding<string>(2)

无界 PubSub(Unbounded PubSub)

无界 PubSub 没有容量限制,因此发布操作始终能立即成功。

无界型 PubSub 保证所有订阅者都能收到所有消息,且不会降低消息交付速率。但如果消息发布速度快于消费速度,其占用的内存可能会无限增长。

通常建议使用有界、丢弃或滑动型 PubSub,除非有特定场景必须使用无界型 PubSub。

示例(创建无界 PubSub)
import { PubSub } from "effect"

// 创建一个容量无限制的无界 PubSub
const unboundedPubSub = PubSub.unbounded<string>()

PubSub 操作符(Operators On PubSubs)

publishAll 方法

PubSub.publishAll 函数允许一次性向 PubSub 发布多个值。

示例(发布多条消息)
import { Effect, PubSub, Queue } from "effect"

const program = Effect.scoped(
  Effect.gen(function* () {
    const pubsub = yield* PubSub.bounded<string>(2)
    const dequeue = yield* PubSub.subscribe(pubsub)
    yield* PubSub.publishAll(pubsub, ["Message 1", "Message 2"])
    console.log(yield* Queue.takeAll(dequeue))
  })
)

Effect.runFork(program)
/*
输出:
{ _id: 'Chunk', values: [ 'Message 1', 'Message 2' ] }
*/

capacity /size 方法

可分别使用 PubSub.capacity 和 PubSub.size 检查 PubSub 的容量和当前大小。

注意,PubSub.capacity 返回一个 number 类型的值,因为容量在 PubSub 创建时就已确定且永不改变。而 PubSub.size 返回一个副作用,用于获取 PubSub 的当前大小,因为 PubSub 中的消息数量会随时间变化。

示例(获取 PubSub 的容量和大小)
import { Effect, PubSub } from "effect"

const program = Effect.gen(function* () {
  const pubsub = yield* PubSub.bounded<number>(2)
  console.log(`capacity: ${PubSub.capacity(pubsub)}`)
  console.log(`size: ${yield* PubSub.size(pubsub)}`)
})

Effect.runFork(program)
/*
输出:
capacity: 2
size: 0
*/

关闭 PubSub(Shutting Down a PubSub)

要关闭 PubSub,可使用 PubSub.shutdown 方法。还可通过 PubSub.isShutdown 验证其是否已关闭,或通过 PubSub.awaitShutdown 等待关闭完成。关闭 PubSub 时,所有关联的队列也会被终止,确保关闭信号能有效传递。

作为入队队列的 PubSub(PubSub as an Enqueue)

PubSub 的操作符与 Queue 类似,主要区别在于用 PubSub.publish 和 PubSub.subscribe 替代了 Queue.offer 和 Queue.take。若你已熟悉 Queue 的使用,会发现 PubSub 非常易于上手。

本质上,PubSub 可看作一个仅允许写入的 Enqueue:

import type { Queue } from "effect"
interface PubSub<A> extends Queue.Enqueue<A> {}

这里的 Enqueue 类型指仅支持入队(或写入)的队列。所有入队到此处的值都会发布到 PubSub,关闭等操作也会对 PubSub 产生影响。

这种设计使 PubSub 具有极高的灵活性,可在任何需要仅接收已发布值的 Enqueue 场景中使用。

信号量(Semaphore)

概述(Overview)

信号量是一种同步机制,用于管理对共享资源的访问。在 Effect 中,信号量可帮助控制资源访问权限,或在异步、并发操作中协调任务执行。

信号量可看作是通用化的互斥锁(mutex),允许并发持有和释放一定数量的 “许可”(permits)。许可类似于 “入场券”,让任务或纤程能够受控地访问共享资源。当没有可用许可时,尝试获取许可的任务会等待,直到有许可被释放。

创建信号量(Creating a Semaphore)

通过 Effect.makeSemaphore 函数初始化信号量,并指定许可数量。每个许可允许一个任务并发访问资源或执行操作,多个许可可实现可配置级别的并发。

示例(创建含 3 个许可的信号量)

import { Effect } from "effect"

// 创建一个含 3 个许可的信号量
const mutex = Effect.makeSemaphore(3)

withPermits 方法

withPermits 方法允许指定运行某个副作用所需的许可数量。当指定数量的许可可用时,副作用会执行,且任务完成后会自动释放这些许可。

示例(使用单许可信号量强制任务顺序执行)

本示例中,三个任务并发启动,但因单许可信号量一次仅允许一个任务执行,最终任务会顺序运行。

import { Effect } 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* Effect.makeSemaphore(1)

  // 包装任务,使其需要 1 个许可,强制顺序执行
  const semTask = mutex
    .withPermits(1)(task)
    .pipe(Effect.withLogSpan("elapsed"))

  // 并发运行 3 个任务,但因单许可信号量,实际顺序执行
  yield* Effect.all([semTask, semTask, semTask], {
    concurrency: "unbounded"
  })
})

Effect.runFork(program)
/*
输出:
timestamp=... level=INFO fiber=#1 message=start elapsed=3ms
timestamp=... level=INFO fiber=#1 message=end elapsed=2010ms
timestamp=... level=INFO fiber=#2 message=start elapsed=2012ms
timestamp=... level=INFO fiber=#2 message=end elapsed=4017ms
timestamp=... level=INFO fiber=#3 message=start elapsed=4018ms
timestamp=... level=INFO fiber=#3 message=end elapsed=6026ms
*/

示例(使用多许可控制并发任务执行)

本示例创建含 5 个许可的信号量,并通过 withPermits(n) 为每个任务分配不同数量的许可:

import { Effect } from "effect"

const program = Effect.gen(function* () {
  const mutex = yield* Effect.makeSemaphore(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" })
})

Effect.runFork(program)
/*
输出:
timestamp=... level=INFO fiber=#1 message="process: 1" elapsed=2011ms
timestamp=... level=INFO fiber=#2 message="process: 2" elapsed=2017ms
timestamp=... level=INFO fiber=#3 message="process: 3" elapsed=4020ms
timestamp=... level=INFO fiber=#4 message="process: 4" elapsed=6025ms
timestamp=... level=INFO fiber=#5 message="process: 5" elapsed=8034ms
*/

门闩(Latch)

概述(Overview)

门闩(Latch)是一种同步工具,功能类似 “闸门”,让纤程等待门闩打开后再继续执行。门闩有两种状态:打开(open)或关闭(closed)。

  • 关闭状态时,到达门闩的纤程会等待,直到门闩打开。
  • 打开状态时,纤程会立即通过。

门闩一旦打开,通常会保持打开状态,不过也可根据需要再次关闭。

举个例子:某个应用需完成初始设置(如加载配置数据、建立数据库连接)后,才能处理请求。可在设置期间创建一个关闭状态的门闩,所有代表请求的纤程都会在门闩处等待。设置完成后,调用 latch.open 即可让请求继续执行。

门闩接口(The Latch Interface)

Latch 包含多个操作,用于控制和观察其状态:

操作描述
whenOpen仅当门闩打开时运行指定的副作用;若门闩关闭,则等待至打开后再执行
open打开门闩,让所有等待的纤程继续执行
close关闭门闩,使后续到达该门闩的纤程进入等待状态
await暂停当前纤程,直到门闩打开;若门闩已打开,则立即返回
release允许等待的纤程继续执行,但不会永久打开门闩

创建门闩(Creating a Latch)

使用 Effect.makeLatch 函数创建门闩,通过传入布尔值指定初始状态(打开或关闭)。默认值为 false,即门闩初始处于关闭状态。

示例(创建和使用门闩)

本示例中,门闩初始为关闭状态。一个纤程仅在门闩打开时打印 “open sesame”。等待 1 秒后,门闩被打开,释放该纤程:

import { Console, Effect } from "effect"

// 演示门闩用法的生成器函数
const program = Effect.gen(function* () {
  // 创建门闩,初始为关闭状态
  const latch = yield* Effect.makeLatch()

  // 分叉一个纤程,仅当门闩打开时打印 "open sesame"
  const fiber = yield* Console.log("open sesame").pipe(
    latch.whenOpen, // 等待门闩打开
    Effect.fork // 将副作用分叉到新纤程
  )

  // 等待 1 秒
  yield* Effect.sleep("1 second")

  // 打开门闩,释放纤程
  yield* latch.open

  // 等待分叉的纤程完成
  yield* fiber.await
})

Effect.runFork(program)
// 输出:open sesame(1 秒后)

门闩与信号量的区别(Latch vs Semaphore)

门闩适用于存在 “一次性事件或条件”,且该事件 / 条件决定纤程能否继续执行的场景。例如,用门闩阻塞所有纤程,直到某个设置步骤完成,之后打开门闩让所有纤程继续。

带一个锁的信号量(通常称为二元信号量或互斥锁),主要用于互斥访问:确保同一时间只有一个纤程能访问共享资源或某段代码。某个纤程获取锁后,其他纤程需等待该锁释放,才能进入受保护区域。

简而言之:

  • 若需让一组纤程等待某个特定事件(“在此等待,直到某个条件成立”),使用门闩(Latch)。
  • 若需确保同一时间只有一个纤程进入临界区或使用共享资源,使用信号量(带一个锁)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值