Skip to main content
Version: ZIO 2.x

FiberRef

FiberRef[A] models a mutable reference to a value of type A. The two basic operations are set, which sets the reference to a new value, and get, which retrieves the current value of the reference.

We can think of FiberRef as Java's ThreadLocal on steroids. So, just like we have ThreadLocal in Java we have FiberRef in ZIO. So as different threads have different ThreadLocals, we can say different fibers have different FiberRefs. They don't intersect or overlap in any way. FiberRef is the fiber version of ThreadLocal with significant improvements in its semantics. A ThreadLocal only has a mutable state in which each thread accesses its own copy, but threads don't propagate their state to their children's.

As opposed to Ref[A], the value of a FiberRef[A] is bound to an executing fiber. Different fibers who hold the same FiberRef[A] can independently set and retrieve values of the reference, without collisions.

import zio._
for {  fiberRef <- FiberRef.make[Int](0)  _        <- fiberRef.set(10)  v        <- fiberRef.get} yield v == 10

Operations#

FiberRef[A] has an API almost identical to Ref[A]. It includes well-known methods such as:

  • FiberRef#get. Returns the current value of the reference.
  • FiberRef#set. Sets the current value of the reference.
  • FiberRef#update / FiberRef#updateSome updates the value with the specified function.
  • FiberRef#modify/ FiberRef#modifySome modifies the value with the specified function, computing a return value for the operation.

You can also use locally to scope FiberRef value only for a given effect:

for {  correlationId <- FiberRef.make[String]("")  v1            <- correlationId.locally("my-correlation-id")(correlationId.get)  v2            <- correlationId.get} yield v1 == "my-correlation-id" && v2 == ""

Propagation#

Let's go back to the FiberRefs analog called ThreadLocal and see how it works. If we have thread A with its ThreadLocal and thread A creates a new thread, let's call it thread B. When thread A sends thread B the same ThreadLocal then what value does thread B see inside the ThreadLocal? Well, it sees the default value of the ThreadLocal. It does not see As value of the ThreadLocal. So in other words, ThreadLocals do not propagate their values across the sort of graph of threads so when one thread creates another, the ThreadLocal value is not propagated from parent to child.

FiberRefs improve on that model quite dramatically. Basically, whenever a child's fiber is created from its parent, the FiberRef value of parent fiber propagated to its child fiber.

Copy-on-Fork#

FiberRef[A] has copy-on-fork semantics for ZIO#fork. This essentially means that a child Fiber starts with FiberRef values of its parent. When the child set a new value of FiberRef, the change is visible only to the child itself. The parent fiber still has its own value.

So if we create a FiberRef and, we set its value to 5, and we pass this FiberRef to a child fiber, it sees the value 5. If the child fiber modifies the value 5 to 6, the parent fiber can't see that change. So the child fiber gets its own copy of the FiberRef, and it can modify it locally. Those changes will not affect the parent fiber:

for {  fiberRef <- FiberRef.make(5)  promise <- Promise.make[Nothing, Int]  _ <- fiberRef    .updateAndGet(_ => 6)    .flatMap(promise.succeed).fork  childValue <- promise.await  parentValue <- fiberRef.get} yield assert(parentValue == 5 && childValue == 6)

join Semantic#

If we join a fiber then its FiberRef is merged back into the parent fiber:

for {  fiberRef <- FiberRef.make(5)  child <- fiberRef.set(6).fork  _ <- child.join  parentValue <- fiberRef.get} yield assert(parentValue == 6)

So if we fork a fiber and that child fiber modifies a bunch of FiberRefs and then later we join it, we get those modifications merged back into the parent fiber. So that's the semantic model of ZIO on join.

Each fiber has its FiberRef and modifying it locally. So when they do their job and join their parent, how do they get merged? By default, the last child fiber will win, the last fiber which is going to join will override the parent's FiberRef value.

As we can see, child1 is the last fiber, so its value which is 6, gets merged back into its parent:

for {  fiberRef <- FiberRef.make(5)  child1 <- fiberRef.set(6).fork  child2 <- fiberRef.set(7).fork  _ <- child2.join  _ <- child1.join  parentValue <- fiberRef.get} yield assert(parentValue == 6)

Custom Merge#

Furthermore we can customize how, if at all, the value will be update when a fiber is forked and how values will be combined when a fiber is merged. To do this you specify the desired behavior during FiberRef#make:

for {  fiberRef <- FiberRef.make(initial = 0, join = math.max)  child    <- fiberRef.update(_ + 1).fork  _        <- fiberRef.update(_ + 2)  _        <- child.join  value    <- fiberRef.get} yield assert(value == 2)

await semantic#

Important to note that await, has no such properties, so await waits for the child fiber to finish and gives us its value as an Exit:

for {  fiberRef <- FiberRef.make(5)  child <- fiberRef.set(6).fork  _ <- child.await  parentValue <- fiberRef.get} yield assert(parentValue == 5)

Join has higher-level semantics that await because it will fail if the child fiber failed, and it will also merge back its value to its parent.

inheritRefs#

We can inherit the values from all FiberRefs from an existing Fiber using the Fiber#inheritRefs method:

for {  fiberRef <- FiberRef.make[Int](0)  latch    <- Promise.make[Nothing, Unit]  fiber    <- (fiberRef.set(10) *> latch.succeed(())).fork  _        <- latch.await  _        <- fiber.inheritRefs  v        <- fiberRef.get} yield v == 10

Note that inheritRefs is automatically called on join. This effectively means that both of the following effects behave identically:

val withJoin =  for {    fiberRef <- FiberRef.make[Int](0)    fiber    <- fiberRef.set(10).fork    _        <- fiber.join    v        <- fiberRef.get  } yield assert(v == 10)
val withoutJoin =  for {    fiberRef <- FiberRef.make[Int](0)    fiber    <- fiberRef.set(10).fork    _        <- fiber.inheritRefs    v        <- fiberRef.get  } yield assert(v == 10)

Memory Safety#

The value of a FiberRef is automatically garbage collected once the Fiber owning it is finished. A FiberRef that is no longer reachable (has no reference to it in user-code) will cause all fiber-specific values of the reference to be garbage collected, even if they were once used in a Fiber that is currently executing.