About celluloid

An actor library in ruby called celluloid is trying a creative approach to the problem of slow operations blocking threads. But, it’s important to be aware of the tradeoffs that are taken to achieve the benefits they tout. The celluloid documentation tells us:

Each actor is a concurrent object running in its own thread, and every
method invocation is wrapped in a fiber that can be suspended whenever
it calls out to other actors, and resumed when the response is available.

Let’s take it for a spin:

# I'm using jruby to run this example

class SlowCounter
  include Celluloid

  def initialize
    @count = 0

  def inc
    temp = @count
    sleep 0.1
    @count = temp + 1

  def get

counter = SlowCounter.new
threads = 10.times.map { Thread.new { 10.times { counter.inc } } }

p counter.get

You might have expected this code to print “100” - 10 threads, each incrementing the counter 10 times. Unfortunately it prints “10”. Celluloid treats the sleep as a “slow” call and the fiber switching mechanism kicks in. When that line is reached the current fiber is suspended and another fiber is spun up to handle another call to this method.

But this fiber again reads in @count to a temporary variable. And the other fiber has not yet updated @count. So now both fibers have temp set to 0. The second fiber gets to the sleep line and yields execution, allowing a third fiber. This continues until all 10 threads have called inc, 10 fibers have been created, each with a 0 in its temp. At that point when the fibers wake up they will write a 1 into @count. Each of them. So the end result after one iteration is 1.

As a side note: celluloid achieves this by overriding what sleep does in actors. You can check that by using Kernel.sleep instead. But of course the point is to simulate what happens when encountering a slow Actor call in another Actor. And this is better simulated by celluloid’s sleep.

One can avoid this by setting your object as “exclusive”:

class SlowCounter
  include Celluloid

# ...

p counter.get # => 100

By doing that you turn off the mechanism allowing for extra fibers. You also lose the benefit of not wasting time waiting for long calls to complete.

I prefer my Actors to be logical threads of execution and to handle messages one by one. Celluloid’s model leaves the problems of synchronizing your objects’ internal state mutations untouched. The exact problems that would normally be solved by adopting Actors.

I like to think about celluloid as more of a plug-and-play concurrency tool. You just take your regular objects, include Celluloid, and presto! The objects are now concurrent. But you still need to pay a lot of attention to the details if these objects manage any kind of internal state.


Now read this

Code as data is just annotated functions?

What I wrote previously might just boil down to having a bunch of annotations on your functions. In a functional language a lot of your functions will look something like this: (defn new-functionality [x] (-> x first-step second-step... Continue →