14

Trying to implement Celluloid async on my working example seem to exhibit weird behavior.

here my code looks

 class Indefinite
    include Celluloid

      def run!
         loop do 
           [1].each do |i|
             async.on_background
           end
         end
      end 


       def on_background
         puts "Running in background" 
       end
   end

   Indefinite.new.run!

but when I run the above code, I never see the puts "Running in Background"

But, if I put a sleep the code seem to work.

class Indefinite
   include Celluloid

    def run! 
      loop do 
        [1].each do |i|
          async.on_background
        end
        sleep 0.5
      end 
    end


   def on_background
     puts "Running in background" 
   end
 end

 Indefinite.new.run!

Any idea? why such a difference in the above two scenario.

Thanks.

Viren
  • 5,812
  • 6
  • 45
  • 98
  • How are you instantiating and calling the class/method? And if it's indefinite, why not `sleep` at the end of the application itself? – digitalextremist Sep 10 '15 at 13:40
  • And why `loop` and `[1].each`? – digitalextremist Sep 10 '15 at 13:40
  • Its and example my friend was trying (this)[ http://stackoverflow.com/questions/32479871/running-code-asynchronously-inside-pollers] at first we thought it's definitely got to do with `ZeroMq` but then I tested the stuff with the above dummy code without any ZeroMq stuff in it and hence found the issue with `celluloid` – Viren Sep 10 '15 at 13:45
  • Both your example and your friend's are missing key pieces. Can you post the entire application in a gist please? – digitalextremist Sep 11 '15 at 13:11
  • @digitalextremist That all I have as an application. If you want you can try this yourself. – Viren Sep 11 '15 at 14:04
  • If that is your whole application, there is no reason to use `async` at all... no reason to use `[1].each` and no reason to use `loop` ... all you need is `sleep` as the last line... you should bring up this situation on the Google Group because it seems like you ( and your friend ) are trying something differently than the code is supposed to work. – digitalextremist Sep 11 '15 at 14:07
  • @digitalextremist you are getting it all wrong this is a sample application. My friend had a very similar looking architecture in his application where the continuous polling for any new message coming onto the ZeroMQ socket. – Viren Sep 12 '15 at 06:34
  • @digitalextremist If we don't have a loop we would never be able to poll zeromq sockets for indefinite time and hence won't be able to receive any new message in future,it is basically doing somewhat like this in Celluloid (https://gist.github.com/meetme2meat/0a38f1e557cb79b84f31) – Viren Sep 12 '15 at 06:46
  • That can be done with `every` and doesn't need `[1].each` ... but I'll come hack with a more complete example later. By the way, your sample code isn't demonstrating the actual behavior you want, so if you can fix it that'd be great. I lead maintenance of each gem involved. – digitalextremist Sep 12 '15 at 06:50
  • @digitalextremist And the reason their is no sleep at the end of the program is because the loop never exit. – Viren Sep 12 '15 at 06:55
  • If you use a loop like that, with async, it's creating infinite background tasks rather than process any. – digitalextremist Sep 12 '15 at 07:05
  • @digitalextremist Yes,But are primary focus currently is to get the async code to run first and also we thought this is something that Celluloid would do this on our behalf. Reason we found exact similar looking example in Celluloid-zmq repo https://github.com/celluloid/celluloid-zmq/blob/master/README.md, look for `def run loop { async.handle_message @socket.read } end` the only reason we had `[].each` block in our example is that we have multiple sockets to listen to whereas the example in `celluloid-zmq` listen to a single socket rest all are same. – Viren Sep 12 '15 at 07:14
  • I've added a second approach that recently came up in the Google Group to my answer. – digitalextremist Sep 16 '15 at 18:48
  • Added a lot more detail for the second and third questions. – digitalextremist Sep 19 '15 at 21:06

3 Answers3

17

Your main loop is dominating the actor/application's threads.

All your program is doing is spawning background processes, but never running them. You need that sleep in the loop purely to allow the background threads to get attention.

It is not usually a good idea to have an unconditional loop spawn infinite background processes like you have here. There ought to be either a delay, or a conditional statement put in there... otherwise you just have an infinite loop spawning things that never get invoked.

Think about it like this: if you put puts "looping" just inside your loop, while you do not see Running in the background ... you will see looping over and over and over.


Approach #1: Use every or after blocks.

The best way to fix this is not to use sleep inside a loop, but to use an after or every block, like this:

every(0.1) {
    on_background
}

Or best of all, if you want to make sure the process runs completely before running again, use after instead:

def run_method
    @running ||= false
    unless @running
        @running = true
        on_background
        @running = false
    end
    after(0.1) { run_method }
 end

Using a loop is not a good idea with async unless there is some kind of flow control done, or a blocking process such as with @server.accept... otherwise it will just pull 100% of the CPU core for no good reason.

By the way, you can also use now_and_every as well as now_and_after too... this would run the block right away, then run it again after the amount of time you want.

Using every is shown in this gist:


The ideal situation, in my opinion:

This is a rough but immediately usable example:


require 'celluloid/current'

class Indefinite
  include Celluloid

  INTERVAL = 0.5
  ONE_AT_A_TIME = true

  def self.run!
    puts "000a Instantiating."
    indefinite = new
    indefinite.run
    puts "000b Running forever:"
    sleep
  end

  def initialize
    puts "001a Initializing."
    @mutex = Mutex.new if ONE_AT_A_TIME
    @running = false
    puts "001b Interval: #{INTERVAL}"
  end

  def run
    puts "002a Running."
    unless ONE_AT_A_TIME && @running
      if ONE_AT_A_TIME
        @mutex.synchronize {
          puts "002b Inside lock."
          @running = true
          on_background
          @running = false
        }
      else
        puts "002b Without lock."
        on_background
      end
    end
    puts "002c Setting new timer."
    after(INTERVAL) { run }
  end


  def on_background
    if ONE_AT_A_TIME
      puts "003 Running background processor in foreground."
    else
      puts "003 Running in background"
    end
  end
end

Indefinite.run!
puts "004 End of application."

This will be its output, if ONE_AT_A_TIME is true:

000a Instantiating.
001a Initializing.
001b Interval: 0.5
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
000b Running forever:
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.

And this will be its output if ONE_AT_A_TIME is false:

000a Instantiating.
001a Initializing.
001b Interval: 0.5
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
000b Running forever:
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.

You need to be more "evented" than "threaded" to properly issue tasks and preserve scope and state, rather than issue commands between threads/actors... which is what the every and after blocks provide. And besides that, it's good practice either way, even if you didn't have a Global Interpreter Lock to deal with, because in your example, it doesn't seem like you are dealing with a blocking process. If you had a blocking process, then by all means have an infinite loop. But since you're just going to end up spawning an infinite number of background tasks before even one is processed, you need to either use a sleep like your question started with, or use a different strategy altogether, and use every and after which is how Celluloid itself encourages you to operate when it comes to handling data on sockets of any kind.


Approach #2: Use a recursive method call.

This just came up in the Google Group. The below example code will actually allow execution of other tasks, even though it's an infinite loop.

This approach is less desirable because it will likely have more overhead, spawning a series of fibers.

def work
    # ...
    async.work
end

Question #2: Thread vs. Fiber behaviors.

The second question is why the following would work: loop { Thread.new { puts "Hello" } }

That spawns an infinite number of process threads, which are managed by the RVM directly. Even though there is a Global Interpreter Lock in the RVM you are using... that only means no green threads are used, which are provided by the operating system itself... instead these are handled by the process itself. The CPU scheduler for the process runs each Thread itself, without hesitation. And in the case of the example, the Thread runs very quickly and then dies.

Compared to an async task, a Fiber is used. So what's happening is this, in the default case:

  1. Process starts.
  2. Actor instantiated.
  3. Method call invokes loop.
  4. Loop invokes async method.
  5. async method adds task to mailbox.
  6. Mailbox is not invoked, and loop continues.
  7. Another async task is added to the mailbox.
  8. This continues infinitely.

The above is because the loop method itself is a Fiber call, which is not ever being suspended ( unless a sleep is called! ) and therefore the additional task added to the mailbox is never an invoking a new Fiber. A Fiber behaves differently than a Thread. This is a good piece of reference material discussing the differences:


Question #3: Celluloid vs. Celluloid::ZMQ behavior.

The third question is why include Celluloid behaves differently than Celluloid::ZMQ ...

That's because Celluloid::ZMQ uses a reactor-based evented mailbox, versus Celluloid which uses a condition variable based mailbox.

Read more about pipelining and execution modes:

That is the difference between the two examples. If you have additional questions about how these mailboxes behave, feel free to post on the Google Group ... the main dynamic you are facing is the unique nature of the GIL interacting with the Fiber vs. Thread vs. Reactor behavior.

You can read more about the reactor-pattern here:

And see the specific reactor used by Celluloid::ZMQ here:

So what's happening in the evented mailbox scenario, is that when sleep is hit, that is a blocking call, which causes the reactor to move to the next task in the mailbox.

But also, and this is unique to your situation, the specific reactor being used by Celluloid::ZMQ is using an eternal C library... specifically the 0MQ library. That reactor is external to your application, which behaves differently than Celluloid::IO or Celluloid itself, and that is also why the behavior is occurring differently than you expected.

Multi-core Support Alternative

If maintaining state and scope is not important to you, if you use jRuby or Rubinius which are not limited to one operating system thread, versus using MRI which has the Global Interpreter Lock, you can instantiate more than one actor and issue async calls between actors concurrently.

But my humble opinion is that you would be much better served using a very high frequency timer, such as 0.001 or 0.1 in my example, which will seem instantaneous for all intents and purposes, but also allow the actor thread plenty of time to switch fibers and run other tasks in the mailbox.

Community
  • 1
  • 1
digitalextremist
  • 5,952
  • 3
  • 43
  • 62
  • 3
    Awesome freaking answer. – Shotgun Ninja Sep 17 '15 at 19:42
  • @digitalextermist Sorry being busy with lot of things on my plate. Before I can accept your can you explain me this.Why ? does `loop { Thread.new { puts 'running' } } ` works then. – Viren Sep 19 '15 at 11:50
  • @digitalextermist And, If given time would you like to explain this as well. [working.rb](https://gist.github.com/meetme2meat/1b2a6f4c7fd04b3d6e93) and [not_working.rb](https://gist.github.com/meetme2meat/e277b0efae9e2a0cdfc2) – Viren Sep 19 '15 at 12:14
  • @Viren at first glance the gists are different because including `Celluloid::ZMQ` uses an "evented" mailbox vs. `Celluloid` which does not. When I have time I will comment on these different but related questions. – digitalextremist Sep 19 '15 at 19:01
  • About using `Thread.new` on MRI, those are run by the CPU scheduler, vs. `async` tasks which by default are `Fiber` based, which must be resumed by the `Actor` on its own thread. A fiber only runs if resumed by a calling thread, vs. a thread which is handled by the interpreter/process/OS. Make sense? – digitalextremist Sep 19 '15 at 19:09
  • @Viren more detail... for an `async` task, that merely puts a new method call in the mailbox of the actor, which must then actually run the task. When you use `Thread.new`, the process schedule immediately prioritizes and runs the block in a thread, even under `MRI` which simulates being multithreaded. But since the `async` task will end up being a fiber in the same thread as the call occurs in, it will not run until there is a break in the fiber currently running. What you are seeing with `sleep` is the calling fiber is being suspended to allow another `async` to run. Fibers behave that way. – digitalextremist Sep 19 '15 at 19:22
  • I updated my answer to include the responses to your additional questions. – digitalextremist Sep 19 '15 at 20:44
  • Added a lot more detail to the third answer. – digitalextremist Sep 19 '15 at 21:06
  • @digitalextremist, based on what you wrote `Since you are using MRI and not jRuby or Rubinius which are not limited to one operating system thread, all your computer is doing is spawning background processes, but never running them.` do you suggest that if we use jRuby @Viren's code will work? – dimakura Sep 20 '15 at 05:00
  • If you have multiple CPU cores, most likely it would. But I would still use the `after` approach. Otherwise it has no back pressure in the system. – digitalextremist Sep 20 '15 at 05:13
  • 2
    @digitalextremist unfortunately it does not. Please see my answer for how I think it can be resolved. – dimakura Sep 20 '15 at 15:01
  • @dimakura pardon, it would work with a two actor setup only, since the single thread processing messages is still blocked. But the answer you provided introduces new (worse) troubles. – digitalextremist Sep 20 '15 at 16:57
  • I've edited my answer to remove the insinuation my answer made that the `GIL` was the main limitation, when it is the domination of the main/actor thread... but in my experience, using non-`GIL` RVM's does present massive improvements, including freeing up `async`, but not in all cases. Even though we all seem to agree a blocking process would be best, the only way to properly solve the issue the OP presented is to use `after` ( vs. `every` or `each` or `loop` ) so a) the task is only run once at a time, if desired, and b) the mailbox is properly throttled, and no 100% core utilization occurs. – digitalextremist Sep 21 '15 at 02:03
4

Let's make an experiment, by modifying your example a bit (we modify it because this way we get the same "weird" behaviour, while making things clearner):

class Indefinite
  include Celluloid

  def run!
    (1..100).each do |i|
      async.on_background i
    end
    puts "100 requests sent from #{Actor.current.object_id}"
  end 

  def on_background(num)
    (1..100000000).each {}
    puts "message #{num} on #{Actor.current.object_id}" 
  end
end

Indefinite.new.run!
sleep

# =>
# 100 requests sent from 2084
# message 1 on 2084
# message 2 on 2084
# message 3 on 2084
# ...

You can run it on any Ruby interpreter, using Celluloid or Celluloid::ZMQ, the result always will be the same. Also note that, output from Actor.current.object_id is the same in both methods, giving us the clue, that we are dealing with a single actor in our experiment.

So there is not much difference between ruby and Celluloid implementations, as long as this experiment is concerned.

Let's first address why this code behaves in this way?

It's not hard to understand why it's happening. Celluloid is receiving incoming requests and saving them in the queue of tasks for appropriate actor. Note, that our original call to run! is on the top of the queue.

Celluloid then processes those tasks, one at a time. If there happens to be a blocking call or sleep call, according to the documentation, the next task will be invoked, not waiting for the current task to be completed.

Note, that in our experiment there are no blocking calls. It means, that the run! method will be executed from the beginning to the end, and only after it's done, each of the on_background calls will be invoked in the perfect order.

And it's how it's supposed to work.

If you add sleep call in your code, it will notify Celluloid, that it should start processing of the next task in queue. Thus, the behavior, you have in your second example.

Let's now continue to the part on how to design the system, so that it does not depend on sleep calls, which is weird at least.

Actually there is a good example at Celluloid-ZMQ project page. Note this loop:

def run
  loop { async.handle_message @socket.read }
end

The first thing it does is @socket.read. Note that it's a blocking operation. So, Celluloid will process to the next message in the queue (if there are any). As soon as @socket.read responds, a new task will be generated. But this task won't be executed before @socket.read is called again, thus blocking execution, and notifying Celluloid to process with the next item on the queue.

You probably see the difference with your example. You are not blocking anything, thus not giving Celluloid a chance to process with queue.

How can we get behavior given in Celluloid::ZMQ example?

The first (in my opinion, better) solution is to have actual blocking call, like @socket.read.

If there are no blocking calls in your code and you still need to process things in background, then you should consider other mechanisms provided by Celluloid.

There are several options with Celluloid. One can use conditions, futures, notifications, or just calling wait/signal on low level, like in this example:

class Indefinite
  include Celluloid

  def run!
    loop do
      async.on_background
      result = wait(:background) #=> 33
    end
  end 

  def on_background
    puts "background" 

    # notifies waiters, that they can continue
    signal(:background, 33)
  end
end

Indefinite.new.run!
sleep

# ...
# background
# background
# background
# ...

Using sleep(0) with Celluloid::ZMQ

I also noticed working.rb file you mentioned in your comment. It contains the following loop:

loop { [1].each { |i|  async.handle_message 'hello' } ; sleep(0) }

It looks like it's doing the proper job. Actually, running it under jRuby revealed, it's leaking memory. To make it even more apparent, try to add a sleep call into the handle_message body:

def handle_message(message)
  sleep 0.5
  puts "got message: #{message}"
end

High memory usage is probably related to the fact, that queue is filled very fast and cannot be processed in given time. It will be more problematic, if handle_message is more work-intensive, then it's now.

Solutions with sleep

I'm skeptical about solutions with sleep. They potentially require much memory and even generate memory leaks. And it's not clear what should you pass as a parameter to the sleep method and why.

dimakura
  • 7,575
  • 17
  • 36
  • You asked about the problems with using multiple actors, some are: no shared scope, vastly increased overhead for entire thread contexts, similar to the first regarding scope - loss of state. I can come back with more if needed. Plus there's a theme developing of using arbitrary values to limit the OP's infinite loop, or reliance on blocking... but neither are needed if a timer is used as I suggested. An `every` loop or `after` block is really best in this scenario, using simple tasks. I've felt alone watching the `celluloid` tag though, and value the time and thought you put into your answer. – digitalextremist Sep 20 '15 at 23:35
  • Also, the use of `signals` ( or better yet `Conditions` ) is ill advised here, because a) it complicates things further, when `sleep` already worked fine in the `OP` example, and b) now there is a return lock and contingency in the code rather than true `async` handling. At that point it might as well be a blocking method call, which negates the whole point of the question. The answer to what the difference between `include Celluloid` and `include Celluloid::ZMQ` is evented/reactor-based mailboxes, versus the usual mailbox; and a custom reactor at that, interfacing with `0MQ` – digitalextremist Sep 21 '15 at 02:09
  • @digitalextremist thanks for your comments. You are right, creating infinite number of actors (or even adding infinite number of tasks into one) creates less desirable state. So I'd thought that blocking mechanism would be of great importance. I'm going to test how your solution with sleep behaves in terms of memory and number of tasks. For my not-experienced eye it looks like memory will be full very fast, like it was with my solution with multiple actors. If it's not the case I will be convinced that sleeps is good as well. – dimakura Sep 21 '15 at 03:48
  • there is also a known memory leak using multi-core compatible RVM's, btw. – digitalextremist Sep 21 '15 at 06:21
  • @digitalextremist using Actor#every doesn't work! Very strange. Also sleep solutions seem to leak memory as well. Is this leak related to rvm only? I tested it with rbenv and rvm as well, both leaking memory considerably. But I don't think it's a bug (of `rvm`/`rbenv`). It was expectable. – dimakura Sep 21 '15 at 06:48
  • Do you get an error, or what does "it doesn't work" mean? If there is a bug, I can resolve it tonight/tomorrow... but if it's an implementation issue, I can show the proper usage. – digitalextremist Sep 21 '15 at 07:02
  • @digitalextremist I mean background method is never invoked. See code in my answer. Did I miss something? – dimakura Sep 21 '15 at 07:03
  • No need for `async` with `every` or `after`; these are essentially `async` blocks already. And my answer suggests `after`, not `every`; it shows the correct usage. Not sure why that is included in your answer now. Using `every` is unneeded compared to `after` by the way, because if there needs to be a protection against overrunning the actor, using `after` is the best way to do that. Also, my answer no longer insinuates what you linked to it for, RE: `GIL` . And use of `server.async.run; sleep` is overkill: if you use `server = Server.new; server.run` that is synchronous: no `sleep` is needed. – digitalextremist Sep 21 '15 at 07:17
  • @digitalextremist approach #1 starts with `The best way to fix this is ... to use an every block`. I just tested it. I've updated as you suggested, still not working (see code above). – dimakura Sep 21 '15 at 07:33
  • Fixed the wording, and will post an `every` example too... but while `every` is better than `loop` by far, it's still not better than `after` in my opinion. – digitalextremist Sep 21 '15 at 07:37
  • Wording is not the problem. I can't make it work. Do you suggest using `every` without a loop? Just like: `def run; every (0.1) { handle_message('hello') } end`? – dimakura Sep 21 '15 at 07:41
  • Here is a gist which works fine with `MRI 2.2.1`... a slight change of my existing `after` example: https://gist.github.com/digitalextremist/686f42e58a58b743142b – digitalextremist Sep 21 '15 at 07:48
  • Using `every` is always without a `loop` as it already is a loop. If you use both together, that will be an amazing way to overload a system. The `every` block behaves like a `loop`, except it doesn't just cycle infinitely at infinite speed ( as fast as the CPU can go ) it loops at a preset speed... which is why you are best off using `after` so that the time to actually run the block is excluded from the loop cycle itself. But in this example the OP wants, running concurrent tasks might be the desired effect, just without the infinite loop behaving in a greedy way, taking all available CPU. – digitalextremist Sep 21 '15 at 07:52
  • Yeah, sorry, I wasn't paying that close attention to your code at first... I am behind on a point release on `Celluloid` ... but what your example is doing is replicating the underlying issue of this problem itself: continually spawning `async` tasks without ever blocking long enough to let another mailbox item be run. But I explained why `every` is the only thing needed, and I posted a working gist. – digitalextremist Sep 21 '15 at 07:56
  • I tested it without loop as well. Strange that the background message in this case is printed only once. No loop behavior. I will test you gist now, maybe it will provide any clue why my code not working. – dimakura Sep 21 '15 at 07:58
  • I've included a `UUID` in the `every` example gist so you can see that it runs multiple background processes. Let me know on Twitter if you need help. – digitalextremist Sep 21 '15 at 08:01
  • 1
    The final problem with your `every` example is that you have no `sleep` occurring at the end of the application, so it really is running only once... then the program exits. – digitalextremist Sep 21 '15 at 10:01
  • 1
    @digitalextremist, yes it was the problem. – dimakura Sep 21 '15 at 10:15
  • @dimakura @digitalextremist Ok finally, I found some time to go through the above answer and comments. Hmm, although the suggestions made above matches to my thought wavelength `where in you guys mentioned having a blocking calls would definitely would not result in such behaviour (sort of in layman's term)` and I agree the above code is my wrong doing(perhaps a very bad example, I took to highlight my friend problem) were in `even the blocking call` you see the same result that is `async` code would never get executed. **Link attached** (here)[http://bit.ly/1LsXU4F] – Viren Sep 22 '15 at 09:44
  • @Viren I liked how digitalextremist described blocking call in one of his comments, calling it "back-pressure". It's exactly like this. If you press your system in one direction you need something to compensate it from another, or your system will blow away. – dimakura Sep 22 '15 at 10:00
  • @dimakura digitalextremist the actual code that never worked with blocking call [https://gist.github.com/meetme2meat/265fd3b1e1021592d0b5](https://gist.github.com/meetme2meat/265fd3b1e1021592d0b5) – Viren Sep 22 '15 at 10:11
  • I answered the other question again. You still have the same problem from this question. The blocking call is not helping you, because after the blocking is over, it hits `async` right away and loops immediately... causing the response never to get processed. Your code over there has several problems though. – digitalextremist Sep 22 '15 at 10:46
2

How threads work with Celluloid

Celluloid is not creating a new thread for each asynchronous task. It has a pool of threads in which it runs every task, synchronous and asynchronous ones. The key point is that the library sees the run! function as a synchronous task, and performs it in the same context than an asynchronous task.

By default, Celluloid runs everything in a single thread, using a queue system to schedule asynchronous tasks for later. It creates new threads only when needed.

Besides that, Celluloid overrides the sleep function. It means that every time you call sleep in a class extending the Celluloid class, the library will check if there are non-sleeping threads in its pool. In your case, the first time you call sleep 0.5, it will create a new Thread to perform the asynchronous tasks in the queue while the first thread is sleeping.

So in your first example, only one Celluloid thread is running, performing the loop. In your second example, two Celluloid threads are running, the first one performing the loop and sleeping at each iteration, the other one performing the background task.

You could for instance change your first example to perform a finite number of iterations:

def run! 
  (0..100).each do
    [1].each do |i|
      async.on_background
    end
  end
  puts "Done!"
end

When using this run! function, you'll see that Done! is printed before all the Running in background, meaning that Celluloid finishes the execution of the run! function before starting the asynchronous tasks in the same thread.

haradwaith
  • 2,701
  • 15
  • 20
  • any clue why does `loop do; Thread.new { puts 'Hello' } end` work then? – Viren Sep 15 '15 at 10:39
  • I've looked more deeply at the way Celluloid works. I've updated my answer, I think it explains now why Celluloid is different from a loop with new threads. – haradwaith Sep 15 '15 at 13:48
  • Not sure where you're getting your information from @haradwaith, but as of 0.17.0 there is no thread pool by default, and individual tasks are performed using fibers by default. The best way to approach this is to use `every` or `after` rather than using `sleep` and finite numbers of `async` tasks. Those approaches do not solve the root issue. – digitalextremist Sep 15 '15 at 23:09
  • 1
    The information comes from Celluloid code itself, which I've read. The fact that Celluloid is using fibers instead of threads doesn't change the overall pool behavior: in the first example it's using one fiber to run all the tasks. The question is not asking how to make the work, because the answer is already in the question: the loop works with `sleep`, which is equivalent here to `every` or `after`. OP is asking *why* there is such a difference between the two examples. – haradwaith Sep 16 '15 at 08:52
  • @bbozo is correct. And I wrote the thread system for Celluloid. What actually happens is Threads/Fibers are spawned and destroyed by default. There is a thread group layer, and on top of that, currently three classes of task class: Fibered, Threaded, Fiber Pool. In my opinion, the Fiber Pool task handler is the most performant, but still, the approach needed to resolve the issue is one of the two I laid out. Also, there is a different thread group class option, which is deprecated: that is the one you are talking about. It's an easy mistake to make. I respect your time investment in Celluloid. – digitalextremist Sep 17 '15 at 23:47