ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Effect v4 同步原语模块化重构:Semaphore 与 Latch 独立成模块的迁移指南

Effect v4 同步原语模块化重构:Semaphore 与 Latch 独立成模块的迁移指南 Effect v4 同步原语模块化重构Semaphore 与 Latch 独立成模块的迁移指南【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effectSemaphore与Latch是 Effect 中协调 fiber 并发行为的两个基础同步原语。在 Effect v4 的演进中它们被从Effect模块中抽离各自独立为顶级模块同时PartitionedSemaphore也被并入Semaphore命名空间。本文以仓库中的变更记录 .changeset/pre/extract-semaphore-latch.md 为主线结合 packages/effect/src/Semaphore.ts、packages/effect/src/Latch.ts 与 packages/effect/src/PartitionedSemaphore.ts 的源码实现完整梳理新 API 形态、用法细节与迁移路径帮助你在升级后快速对齐新写法。变更概览一次 patch 级别的模块化重构该 changeset 声明了一个effect: patch级别的变更说明这是一次非破坏性的重构——旧 API 不会被立即移除但新增了更清晰、更内聚的模块组织方式。核心变更可归纳为三条Semaphore与Latch被提取为各自独立的模块不再寄生在Effect模块内部构造函数的命名统一新增Semaphore.make/Semaphore.makeUnsafe与Latch.make/Latch.makeUnsafe分别替代原先的Effect.makeSemaphore/Effect.makeSemaphoreUnsafe与Effect.makeLatch/Effect.makeLatchUnsafePartitionedSemaphore被并入Semaphore模块以Semaphore.Partitioned、Semaphore.makePartitioned、Semaphore.makePartitionedUnsafe的形式重新导出。从源码结构看三个模块的实体定义均已落在独立文件中packages/effect/src/Semaphore.ts、packages/effect/src/Latch.ts、packages/effect/src/PartitionedSemaphore.ts且统一标注since 4.0.0与本次模块化重构的版本节点一致。这次调整让原语的职责边界更清晰Semaphore聚焦“许可数量控制”Latch聚焦“开关/放行信号”使用者可以按需引入而不是从庞大的Effect模块中翻找。Semaphore独立的许可Permit控制模块Semaphore的本质是一个计数信号量它持有一批许可permits任务必须先获取到足够数量的许可才能执行执行完毕后归还许可从而把共享资源的并发访问限制在指定上限内。源码中的SemaphoreImpl用三个字段维护状态packages/effect/src/Semaphore.tspermits信号量总许可数容量taken当前已被占用的许可数waiters等待许可的观察者集合许可释放时会被唤醒。可用许可数由free permits - taken动态计算。构造make 与 makeUnsafe新 API 提供两个构造函数与旧 API 一一对应新 API旧 API说明Semaphore.make(permits)Effect.makeSemaphore(permits)在 Effect 内部创建返回EffectSemaphoreSemaphore.makeUnsafe(permits)Effect.makeSemaphoreUnsafe(permits)同步创建直接返回Semaphore值两者都以指定数量的许可初始化信号量。make适合在Effect.gen中用yield*获取makeUnsafe适合在 Effect 工作流之外如模块顶层提前分配import { Effect, Semaphore } from effect // 在 Effect 内部创建许可总数为 2 const program Effect.gen(function*() { const semaphore yield* Semaphore.make(2) return yield* semaphore.withPermits(1)(Effect.succeed(Resource accessed)) }) await Effect.runPromise(program) // Resource accessedimport { Effect, Semaphore } from effect // 在 Effect 之外同步创建许可总数为 3 const semaphore Semaphore.makeUnsafe(3) const task (id: number) semaphore.withPermits(1)( Effect.gen(function*() { yield* Effect.yieldNow return id }) ) // 5 个任务并发执行但同一时刻最多只有 3 个在运行 const program Effect.all([task(1), task(2), task(3), task(4), task(5)], { concurrency: unbounded }) await Effect.runPromise(program) // [1, 2, 3, 4, 5]自动获取与释放withPermits / withPermit日常使用中最推荐的方式是withPermits系列它把“获取许可 → 运行 effect → 归还许可”封装成一个整体即使被包裹的 effect 失败或被中断许可也会通过onExitPrimitive在退出路径上归还见 packages/effect/src/Semaphore.ts 的实现。withPermits(n)(effect)获取n个许可后运行 effect结束后自动释放withPermit(effect)withPermits(1)的单许可便捷形式withPermitsIfAvailable(n)(effect)非阻塞变体——许可立即可得才运行否则直接返回Option.none且不执行 effect成功时结果包装为Option.some。import { Effect, Semaphore } from effect const program Effect.gen(function*() { const semaphore yield* Semaphore.make(2) const task (id: number) semaphore.withPermit( Effect.gen(function*() { yield* Effect.yieldNow return id }) ) // 4 个任务并发执行但同一时刻最多只有 2 个在运行 return yield* Effect.all([task(1), task(2), task(3), task(4)]) }) await Effect.runPromise(program) // [1, 2, 3, 4]withPermitsIfAvailable的典型场景是“有额度就做没额度就跳过”的降级逻辑例如限流队列中的非关键任务import { Effect, Semaphore } from effect const program Effect.gen(function*() { const semaphore yield* Semaphore.make(1) // 占用唯一许可 yield* semaphore.withPermit(Effect.sleep(10 millis)) // 许可已被占用立即返回 None不执行 effect const result yield* semaphore.withPermitsIfAvailable(1)(Effect.succeed(task)) return result }) await Effect.runPromise(program) // none手动协议take / release / releaseAll当许可的获取与释放无法限定在单个 effect 生命周期内时可以使用底层手动协议take(n)等待并获取n个许可返回实际获取的数量不足时挂起等待takeIfAvailable(n)许可立即可得则获取并返回true否则立即返回false绝不等待release(n)归还n个许可唤醒等待者返回当前可用许可数releaseAll一次性归还所有被占用的许可返回当前可用许可数。源码中的等待逻辑值得注意take通过waitForPermits注册观察者只有free n时才放行而takeIfAvailable则直接检查free n并立即短路返回falsepackages/effect/src/Semaphore.ts。release时若存在等待者会经由当前 fiber 的调度器以scheduleTask(..., 0)批量唤醒满足条件的观察者packages/effect/src/Semaphore.ts。使用手动协议时必须自己保证take与release的计数平衡源码注释明确提示能用一个 effect 限定获取范围时优先使用withPermit/withPermits手动协议留给更底层的协调场景。动态调整容量resizeresize(permits)可以动态改变既有信号量的总许可数而保持已获取的许可不变。如果新容量小于当前已占用数free 0新获取请求会一直等待直到许可被归还反之会立即唤醒等待者packages/effect/src/Semaphore.ts。这适合运行时弹性伸缩并发上限的场景例如根据负载调整数据库连接池大小。Latch可复用的开/关放行信号Latch是一个“要么开、要么关”的协调原语关闭时await与whenOpen会挂起打开后当前及未来的等待者都能继续执行。与一次性栅栏不同Latch是可复用的——close可以再次关闭它让后续等待者重新挂起。Latch的模块头注释将它定位为“可复用的 fiber 协调同步原语”适用于必须等待显式打开或放行信号才能继续的场景packages/effect/src/Latch.ts。构造与核心方法构造函数同样提供 effectful 与同步两种形态新 API旧 API说明Latch.make(open?)Effect.makeLatch(open?)在 Effect 内部创建默认关闭Latch.makeUnsafe(open?)Effect.makeLatchUnsafe(open?)同步创建默认关闭传true则初始为打开Latch的核心方法及其语义如下完整接口见 packages/effect/src/Latch.ts方法语义返回值open/openUnsafe打开闩锁放行当前和未来的所有等待者boolean本次调用是否使闩锁由关变开release只放行当前等待者不打开闩锁boolean释放请求是否在关闭状态下发出close/closeUnsafe再次关闭使未来等待者重新挂起boolean本次调用是否使闩锁由开变关await挂起当前 fiber直到闩锁被打开或当前等待者被释放voidwhenOpen(effect)先等待放行再运行给定 effect保留其成功/失败/依赖类型被包裹 effect 的结果isOpen不挂起、不改变状态地检查当前开/关状态boolean一个典型的协调示例——让一个 fiber 等待另一个 fiber 发信号import { Effect, Fiber, Latch } from effect const program Effect.gen(function*() { const latch yield* Latch.make() const waiter yield* Effect.forkChild(latch.await.pipe(Effect.as(opened))) yield* latch.open return yield* Fiber.join(waiter) }) await Effect.runPromise(program) // opened需要区分open与release的差异open是“永久放行”直到再次close而release是“只放行当前批次”后续调用await仍会挂起。这在“批量同步、分批放行”的场景中非常有用。源码实现要点Latch的内部实现位于 packages/effect/src/internal/effect.ts内部用_isOpen布尔值与waiters以及用于release调度的scheduled数组维护状态open使用withFiber获取当前 fiber再通过scheduleUnsafe调度唤醒openUnsafe则同步翻转状态并flushWaiters()await在闩锁已打开时立即完成否则把 resume 回调压入waiters并返回一个可取消等待的清理函数closeUnsafe只翻转_isOpen而不唤醒任何人——这正是“关闭只影响未来等待者”语义的来源whenOpen在实现上就是flatMap(this.await, () self)即先等信号再执行 effectpackages/effect/src/internal/effect.ts。flushWaiters的注释还揭示了一个细节唤醒前会先把waiters数组整体换出因为被唤醒的等待者可能重入地再次关闭闩锁并注册新等待者这些新等待者不能被本次 flush 误唤醒packages/effect/src/internal/effect.ts。这个设计保证了重入场景下的正确性。PartitionedSemaphore 并入 Semaphore 命名空间PartitionedSemaphore解决的是“共享许可池 分区公平性”的问题多个独立分组的工作竞争同一份有界资源时等待许可的请求按**分区键key分组释放许可时按轮询round-robin**顺序在等待的分区间分配避免某个繁忙分组垄断刚释放的许可packages/effect/src/PartitionedSemaphore.ts。变更记录明确给出了合并后的命名形式新 API说明Semaphore.Partitioned类型别名等价于PartitionedSemaphoreKSemaphore.makePartitioned(options)在 Effect 内部创建分区信号量Semaphore.makePartitionedUnsafe(options)同步创建分区信号量PartitionedSemaphore的接口packages/effect/src/PartitionedSemaphore.ts包含capacity固定总容量、available当前可用许可、以及带分区键的take(key, permits)/release(permits)/withPermits(key, permits)/withPermit(key)等方法语义与普通Semaphore对齐只是获取时多了一个分区键参数import { Effect, Semaphore } from effect const program Effect.gen(function*() { const semaphore yield* Semaphore.makePartitioned({ permits: 4 }) // 每个分区键对应一组独立排队的工作 const run (key: string) semaphore.withPermit(key)(Effect.succeed(partition ${key} done)) return yield* Effect.all([run(a), run(b), run(c)], { concurrency: unbounded }) }) await Effect.runPromise(program)实现层面的几个行为边界值得注意packages/effect/src/PartitionedSemaphore.ts负许可数被钳制为 0非有限许可数如Infinity创建无界信号量获取与释放立即完成等待者通过MutableHashMap按分区键分组存储释放许可时用迭代器按轮询顺序为各分区逐个配发take请求的许可数超过总容量时永远不会完成maxPermits permits直接挂起为Effect.never请求 0 或负数许可时直接放行不占用任何许可。迁移路径从 Effect.* 到模块化 API本次变更是一次纯重命名式的迁移行为不变只需调整导入与调用位置。对照关系如下迁移前旧写法迁移后新写法Effect.makeSemaphore(2)Semaphore.make(2)Effect.makeSemaphoreUnsafe(2)Semaphore.makeUnsafe(2)Effect.makeLatch()Latch.make()Effect.makeLatchUnsafe(false)Latch.makeUnsafe(false)PartitionedSemaphore.make({ permits: 4 })Semaphore.makePartitioned({ permits: 4 })PartitionedSemaphore.makeUnsafe({ permits: 4 })Semaphore.makePartitionedUnsafe({ permits: 4 })对应的导入变为// 迁移前 import { Effect } from effect const semaphore Effect.makeSemaphore(2) // 迁移后 import { Effect, Semaphore, Latch } from effect const semaphore Semaphore.make(2) const latch Latch.make()由于变更等级为patch旧 API 在新版本中仍可用你可以渐进式迁移先引入新模块写法并验证行为一致再择机清理旧调用。迁移完成后这三个原语的使用将统一收敛到Semaphore/Latch两个顶级模块之下配合Semaphore.Partitioned命名空间形成一个完整、内聚的并发同步工具集。小结Semaphore与Latch已成为独立的顶级模块构造函数统一为make/makeUnsafe形态PartitionedSemaphore并入Semaphore以Semaphore.Partitioned系列命名空间形式提供自动管理许可优先使用withPermits/withPermit/withPermitsIfAvailable手动协议take/release/releaseAll仅用于无法限定获取范围的底层场景且必须自行保证计数平衡Latch的open与release语义不同close只影响未来等待者迁移为非破坏性变更新旧写法可共存按上表逐项替换即可。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表