多个状态要一起改,用 Ref、Semaphore 还是 Tx?

两个状态要一起改的时候,第一反应是上事务,但我多数时候先不上。

我的判断顺序是:单个值用 Ref;多个值能合并成一个值,就合并成一个 Ref;多个独立结构要一起改、或者要等条件,才用 Tx;要限制并发度、临界区里有异步副作用,用 Semaphore。

这个顺序的依据是:每往右一步,机制更重,也多一层要理解的行为。Tx 是 v4 里取代 STM 的事务构造,背景见 Effect v4 没有 STM 了,怎么原子地改多个状态?,下面按场景一条条说。

什么时候 Ref 就够了?

只有一个值:用 Ref。Ref.modify 已经是原子的,没有理由引入事务。

多个值要保持一致,且能合并成一个值:优先合并成一个 Ref 放一个对象,比如 Ref<{ a: number; b: number }>,一次 modify 搞定。这是最便宜的方案,我会先试它。

什么时候该上 Tx?

多个独立的值或数据结构,需要一起改,或者需要「等条件」:这才是 Tx 的主场。txRetry 把条件等待和原子更新放进同一个构造里,不用自己拼 Deferred、Semaphore、重试循环,见 Effect.txRetry 怎么让事务等条件满足?。

另一点务实的看法:Tx 结构是进程内、内存里的。它不是数据库事务,不跨进程,不持久化;需要跨实例一致性时,应该用数据库事务,见 Effect 的 SQL 与 RPC:Schema 驱动的数据模型、事务与共享契约。

要限制并发度,该用 Semaphore 还是 TxSemaphore?

要限制并发度,且临界区里有任意的异步副作用:用普通的 Semaphore,用法见 Effect 里怎么用 Semaphore 限制并发数?。原因是事务体可能重跑,里面的副作用会重复,不能当互斥区用,细节见 Effect.tx 事务体里有哪些坑?。

Semaphore 保证临界区只执行一次,Tx 保证状态变更原子,两者解决的不是一个问题。

TxSemaphore 的位置有点微妙。它的许可计数本身是事务状态,所以它可以和别的 Tx 结构放进同一个事务。不过我只单独验证过 withPermit 的并发上限,组合用法没有跑过。如果你只是想限流一段异步调用,普通 Semaphore 更直接。

Ref、Queue、PubSub 这些别的共享状态原语呢?

这篇只比较「一起改多个状态」的几种做法。单个原语(Ref、Queue、PubSub、Semaphore)各自的适用场景,见 Effect 里 Ref、Queue、PubSub、Semaphore 怎么选?。

版本是 effect@4.0.5。这里的判断顺序是我的个人取舍,不是官方推荐;TxSemaphore.withPermit 配 2 个许可、6 个并发任务时同时运行数峰值为 2,在本地跑过,TxSemaphore 与其他 Tx 结构组合进同一个事务没有验证。Ref.modify 的原子性和普通 Semaphore 的行为来自对 Effect 文档的理解,没有在这篇里重新运行。