在 Cats Effect 3 中,它们提供了并发原语,如
Ref
和 AtomicCell
。但我不确定两者之间有什么不同。
什么时候我们需要
AtomicCell
而不是Ref
?它们有什么特点?缺少哪些功能?
让我们从比较 API 开始:
Ref
定义类似:的方法
def access: F[(A, (A) => F[Boolean])]
def modify[B](f: (A) => (A, B)): F[B]
def modifyState[B](state: State[A, B]): F[B]
def set(a: A): F[Unit]
def tryModify[B](f: (A) => (A, B)): F[Option[B]]
def tryModifyState[B](state: State[A, B]): F[Option[B]]
def tryUpdate(f: (A) => A): F[Boolean]
def update(f: (A) => A): F[Unit]
// and methods build on top of them
这些方法要求我们提供一个纯函数来修改状态,或者只从中读取/写入一个值。这就像
var
的更安全版本。
AtomicCell
定义类似:的方法
def evalGetAndUpdate(f: (A) => F[A]): F[A]
def evalModify[B](f: (A) => F[(A, B)]): F[B]
def evalUpdate(f: (A) => F[A]): F[Unit]
def evalUpdateAndGet(f: (A) => F[A]): F[A]
def get: F[A]
def modify[B](f: (A) => (A, B)): F[B]
def set(a: A): F[Unit]
// and methods build on top of them
它允许使用适用于
F
的方法更新状态 - 也就是说,您的状态可以通过副作用进行更新。所以这就像一个更安全的:
private var a: A
def update(f: A => A): A = synchronized {
a = f(a)
}
为什么这很重要? 文档告诉我们原因:
AtomicCell 可以被视为 Mutex 和 Ref 的组合:
import cats.effect.{IO, Ref} import cats.effect.std.Mutex trait State class Service(mtx: Mutex[IO], ref: Ref[IO, State]) { def modify(f: State => IO[State]): IO[Unit] = mtx.lock.surround { for { current <- ref.get next <- f(current) _ <- ref.set(next) } yield () } }
以下内容与上面的示例等效:
import cats.effect.IO import cats.effect.std.AtomicCell trait State class Service(cell: AtomicCell[IO, State]) { def modify(f: State => IO[State]): IO[Unit] = cell.evalUpdate(current => f(current)) }
换句话说:
Ref
是“更精简”,但只有在不需要“事务”的情况下才方便使用,并且不存在并发访问破坏事物的风险(例如,线程 1 读取并开始阻塞计算,线程 2 读取并开始阻塞计算)。开始阻塞计算,线程 2 写入,线程 1 写入...1 个更新已丢失!)AtomicCell
正在阻塞新的更新,直到开始的更新完成(线程 1 读取并开始长计算,线程 2 尝试读取...并且必须等待,线程 1 完成,线程 2 可以读取值...),所以使用起来应该更安全,但互斥可能会带来一些开销对于在单个
Ref
中处理的事情,或者不太可能并发更新的事情(例如,使用管理命令更改一些全局标志),我可能更喜欢Fiber
。或者在测试和原型设计期间进行简单的模拟。
当我必须将状态保留在内存中,同时将其视为具有一定事务性但不一定具有持久性的简单“内存数据库”时,我会选择
AtomicCell
(与STM类似的用例,但在这里我们允许副作用,所以退休是不可能的)。