Deferred
掌握用 Deferred 进行异步协调的方法:这种一次性变量可用于管理 effect 的同步与通信。
Deferred<Success, Error> 是 Effect 的一个特殊子类型,它的行为就像一个带有些许独特之处的一次性变量。它只能被完成一次,因此是管理异步操作以及程序不同部分之间同步的有力工具。
Deferred 本质上是一种同步原语,用来表示一个可能不会立即可用的值。当你创建一个 Deferred 时,它一开始是空的。之后,它可以用一个成功值 Success 或一个错误值 Error 来完成:
┌─── Represents the success type
│ ┌─── Represents the error type
│ │
▼ ▼
Deferred<Success, Error>
一旦完成,它就不能再被更改。
当一个 Fiber 调用 Deferred.await 时,它会暂停,直到该 Deferred 被完成。在 Fiber 等待期间,它并不会阻塞线程,而只是在语义上阻塞。这意味着其他 Fiber 仍然可以运行,从而保证了高效的并发。
Deferred 在概念上类似于 JavaScript 的 Promise。
关键区别在于它同时支持成功类型和错误类型,从而提供了更强的类型安全。
创建 Deferred
可以用 Deferred.make 构造器创建 Deferred。它返回一个表示「创建 Deferred」这一过程的 effect。由于创建 Deferred 涉及内存分配,因此必须在 effect 内部完成,以确保对资源的安全管理。
示例(创建一个 Deferred)
import { Deferred, Effect } from "effect"
// ┌─── Effect<Deferred<string, Error>>
// ▼
const deferred = Deferred.make<string, Error>()
const d = await Effect.runPromise(deferred)
Deferred.isDeferred(d) // => true
等待
要从 Deferred 中取出值,可以使用 Deferred.await。这个操作会挂起调用它的 Fiber,直到该 Deferred 以一个值或一个错误被完成。
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))
// The Deferred is never completed, so awaiting it suspends the fiber
const result = await Effect.runPromise(
Effect.race(value, Effect.sleep("50 millis").pipe(Effect.as("timeout"))),
)
result // => "timeout"
完成 Deferred
你可以用多种方式完成一个 Deferred,具体取决于你想让它成功、失败,还是中断正在等待的 Fiber:
| API | 说明 |
|---|---|
Deferred.succeed | 用一个值成功完成该 Deferred。 |
Deferred.done | 用一个 Exit 值完成该 Deferred。 |
Deferred.complete | 用某个 effect 的结果完成该 Deferred。 |
Deferred.completeWith | 用一个 effect 完成该 Deferred。该 effect 会被每一个正在等待的 Fiber 执行,因此请谨慎使用。 |
Deferred.fail | 用一个错误使该 Deferred 失败。 |
Deferred.die | 用一个用户定义的错误使该 Deferred 产生 defect。 |
Deferred.failCause | 用一个 Cause 使该 Deferred 失败或产生 defect。 |
Deferred.interrupt | 中断该 Deferred,强制停止或中断正在等待的 Fiber。 |
示例(用一个成功值完成 Deferred)
import { Effect, Deferred } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
// Complete the Deferred successfully
yield* Deferred.succeed(deferred, 1)
// Awaiting the Deferred to get its value
const value = yield* Deferred.await(deferred)
console.log(value)
value // => 1
})
await Effect.runPromise(program)
完成一个 Deferred 会产生一个 Effect<boolean>。如果该 Deferred 被成功完成,这个 effect 返回 true;如果它之前已经被完成过,则返回 false。这对于跟踪 Deferred 的状态很有用。
示例(检查完成状态)
import { Effect, Deferred } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
// Attempt to fail the Deferred
const firstAttempt = yield* Deferred.fail(deferred, "oh no!")
// Attempt to succeed after it has already been completed
const secondAttempt = yield* Deferred.succeed(deferred, 1)
console.log([firstAttempt, secondAttempt])
const both = [firstAttempt, secondAttempt] // => [true, false]
})
await Effect.runPromise(program)
检查完成状态
有时,你可能需要在不挂起 Fiber 的情况下检查一个 Deferred 是否已经完成。这可以用 Deferred.poll 方法做到。它的工作方式如下:
Deferred.poll返回一个Option<Effect<A, E>>:- 如果
Deferred尚未完成,它返回None。 - 如果
Deferred已完成,它返回Some,其中包含结果或错误。
- 如果
此外,你可以用 Deferred.isDone 函数检查一个 Deferred 是否已经完成。该方法返回一个 Effect<boolean>,如果 Deferred 已完成,它就求值为 true,让你能够快速检查它的状态。
示例(轮询并检查完成状态)
import { Effect, Deferred, Option } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
// Polling the Deferred to check if it's completed
const done1 = yield* Deferred.poll(deferred)
// Checking if the Deferred has been completed
const done2 = yield* Deferred.isDone(deferred)
console.log([done1, done2])
const both = [done1, done2] // => [Option.none(), false]
})
await Effect.runPromise(program)
常见用例
当你需要等待程序中某件特定的事情发生时,Deferred 就派上了用场。
它非常适合这样的场景:你希望代码的某一部分在就绪时向另一部分发出信号。
以下是一些常见用例:
| 用例 | 说明 |
|---|---|
| 协调 Fiber | 当你有多个并发任务并需要协调它们的动作时,Deferred 可以帮助一个 Fiber 在完成自己的任务时向另一个 Fiber 发出信号。 |
| 同步 | 每当你希望确保某一段代码在另一段代码完成工作之前不继续执行时,Deferred 都能提供你所需要的同步。 |
| 交接工作 | 你可以用 Deferred 把工作从一个 Fiber 移交到另一个 Fiber。例如,一个 Fiber 准备好一些数据,然后第二个 Fiber 继续处理它。 |
| 挂起执行 | 当你希望一个 Fiber 暂停执行直到某个条件满足时,可以用 Deferred 阻塞它,直到该条件被满足。 |
示例(用 Deferred 协调两个 Fiber)
在这个示例中,我们用 Deferred 在两个 Fiber 之间传递一个值。
通过并发运行这两个 Fiber,并把 Deferred 用作同步点,我们可以确保 fiberB 只有在 fiberA 完成自己的任务之后才继续执行。
import { Effect, Deferred, Fiber } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<string, string>()
// Completes the Deferred with a value after a delay
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")
})
// Waits for the Deferred and prints the value
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
})
// Run both fibers concurrently
const fiberA = yield* Effect.forkChild(taskA)
const fiberB = yield* Effect.forkChild(taskB)
// Wait for both fibers to complete
const both = yield* Effect.zip(Fiber.join(fiberA), Fiber.join(fiberB))
console.log(both)
both // => [true, "hello world"]
})
await Effect.runPromise(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' ]
*/