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
end
def inc
temp = @count
sleep 0.1
@count = temp + 1
end
def get
@count
end
end
counter = SlowCounter.new
threads = 10.times.map { Thread.new { 10.times { counter.inc } } }
threads.each(&:join)
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
exclusive
# ...
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.