New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Questions about Concurrent Ruby #379

Closed
jdantonio opened this Issue Jul 21, 2015 · 8 comments

Comments

3 participants
@jdantonio
Member

jdantonio commented Jul 21, 2015

With the Riding Rails announcement last week and this discussion over at ActionCable, there are a lot of people talking about concurrent-ruby. Please use this thread for general questions about who we are, what we do, and what we provide.

@jdantonio jdantonio added the question label Jul 21, 2015

@jdantonio jdantonio self-assigned this Jul 21, 2015

@Asmod4n

This comment has been minimized.

Show comment
Hide comment
@Asmod4n

Asmod4n Jul 21, 2015

When you want to run, say, 500 blocking operations at once (I/O Server), do you still need 500 actors?

Asmod4n commented Jul 21, 2015

When you want to run, say, 500 blocking operations at once (I/O Server), do you still need 500 actors?

@jdantonio

This comment has been minimized.

Show comment
Hide comment
@jdantonio

jdantonio Jul 22, 2015

Member

@Asmod4n The best person to answer your question is @pitr-ch since he built our actor implementation, but the short answer is Yes. But this also raises the question, is actor the best abstraction for your problem?

Like all our high-level abstractions, our actors run on one of the global thread pools. (You can also use dependency injection to provide a custom thread pool if you like.) Internally, however, each actor uses a class called SerializedExecution to ensure that each actor performs all its assigned tasks in the order they were received--one at a time. This behavior is identical to both Akka's actor and Erlang's gen_server. A single actor does one thing at a time, it just does so concurrently with respect to everything else in the system.

If you want to run many I/O tasks simultaneously, we have other high-level abstractions that may be suitable for the task. A set of Futures is very common. Consider this code:

require 'concurrent'

stock_symbols = [...] # provide a list of stock symbols here

futures = symbols.collect do |stock_symbol|
  Concurrent::Future.execute { FinanciApi.get_stock_price(stock_symbol) } # do the I/O operation here
end

stock_prices = futures.collect {|future| future.value } # iterate over all futures and get the values

The above code will run all the futures on the GLOBAL_IO_EXECUTOR which is a thread pool with an unbound size and unbound queue length. It will add as many threads as it needs (until the OS won't let it create any more--around 2000 on OS X, more on Linux). In the above example, all the I/O operations should run concurrently.

This highlights one of the biggest advantages of concurrent-ruby over other Ruby concurrency libraries. We are intentionally "unopinionated." We don't try to sell our users on the fallacy that there's a "one size fits all" concurrency solution. We provide a broad and deep toolkit that allows you to choose the best tool for the job.

Member

jdantonio commented Jul 22, 2015

@Asmod4n The best person to answer your question is @pitr-ch since he built our actor implementation, but the short answer is Yes. But this also raises the question, is actor the best abstraction for your problem?

Like all our high-level abstractions, our actors run on one of the global thread pools. (You can also use dependency injection to provide a custom thread pool if you like.) Internally, however, each actor uses a class called SerializedExecution to ensure that each actor performs all its assigned tasks in the order they were received--one at a time. This behavior is identical to both Akka's actor and Erlang's gen_server. A single actor does one thing at a time, it just does so concurrently with respect to everything else in the system.

If you want to run many I/O tasks simultaneously, we have other high-level abstractions that may be suitable for the task. A set of Futures is very common. Consider this code:

require 'concurrent'

stock_symbols = [...] # provide a list of stock symbols here

futures = symbols.collect do |stock_symbol|
  Concurrent::Future.execute { FinanciApi.get_stock_price(stock_symbol) } # do the I/O operation here
end

stock_prices = futures.collect {|future| future.value } # iterate over all futures and get the values

The above code will run all the futures on the GLOBAL_IO_EXECUTOR which is a thread pool with an unbound size and unbound queue length. It will add as many threads as it needs (until the OS won't let it create any more--around 2000 on OS X, more on Linux). In the above example, all the I/O operations should run concurrently.

This highlights one of the biggest advantages of concurrent-ruby over other Ruby concurrency libraries. We are intentionally "unopinionated." We don't try to sell our users on the fallacy that there's a "one size fits all" concurrency solution. We provide a broad and deep toolkit that allows you to choose the best tool for the job.

@jdantonio

This comment has been minimized.

Show comment
Hide comment
@jdantonio

jdantonio Jul 22, 2015

Member

@Asmod4n On point of clarification, it's probably not a good idea to run 500 I/O operations concurrently. There are performance costs associated with creating and managing a large number of threads. This is a situation where our ability to inject a custom executor is very handy:

pool = Concurrent::FixedThreadPool.new(10)

futures = symbols.collect do |stock_symbol|
  Concurrent::Future.execute(executor: pool) { FinanciApi.get_stock_price(stock_symbol) }
end
Member

jdantonio commented Jul 22, 2015

@Asmod4n On point of clarification, it's probably not a good idea to run 500 I/O operations concurrently. There are performance costs associated with creating and managing a large number of threads. This is a situation where our ability to inject a custom executor is very handy:

pool = Concurrent::FixedThreadPool.new(10)

futures = symbols.collect do |stock_symbol|
  Concurrent::Future.execute(executor: pool) { FinanciApi.get_stock_price(stock_symbol) }
end
@Asmod4n

This comment has been minimized.

Show comment
Hide comment
@Asmod4n

Asmod4n Jul 22, 2015

@jdantonio thats what i was looking for :)

Asmod4n commented Jul 22, 2015

@jdantonio thats what i was looking for :)

@pitr-ch

This comment has been minimized.

Show comment
Hide comment
@pitr-ch

pitr-ch Jul 25, 2015

Member

Another approach (it would be interesting to compare the performance) could be to use some event loop lib (e.g. EventMachine) to deal with the IO, doing only necessary work in callbacks, distributing rest of the work to actors, futures, etc.

Member

pitr-ch commented Jul 25, 2015

Another approach (it would be interesting to compare the performance) could be to use some event loop lib (e.g. EventMachine) to deal with the IO, doing only necessary work in callbacks, distributing rest of the work to actors, futures, etc.

@Asmod4n

This comment has been minimized.

Show comment
Hide comment
@Asmod4n

Asmod4n Jul 25, 2015

Call be uninformed, but doesn't all I/O have to be handled by EventMachine for the current Process then?

Asmod4n commented Jul 25, 2015

Call be uninformed, but doesn't all I/O have to be handled by EventMachine for the current Process then?

@pitr-ch

This comment has been minimized.

Show comment
Hide comment
@pitr-ch

pitr-ch Jul 26, 2015

Member

It might be better to do it all uniformly on EventMachine but it's not required. E.g. all http requests could go through EM where translated to messages and then to actors for processing, but other parts of the code can use IO blocking operations to access files or other stuff without EM.

Member

pitr-ch commented Jul 26, 2015

It might be better to do it all uniformly on EventMachine but it's not required. E.g. all http requests could go through EM where translated to messages and then to actors for processing, but other parts of the code can use IO blocking operations to access files or other stuff without EM.

@jdantonio

This comment has been minimized.

Show comment
Hide comment
@jdantonio

jdantonio Jul 27, 2015

Member

EventMachine suggests that all concurrency be handled by EM, but that's not a requirement. EM just uses Ruby threads under-the-hood so you can mix-and-match. You just have to know what you are doing. The flow @pitr-ch suggests should work fine. The basic application flow would be:

  1. General initialization/configuration
  2. Start actors for processing received messages
  3. Start the EM reactor
    • Create I/O listeners
    • When a message is received, pass to appropriate actor for handling
  4. Stop the EM reactor when shutdown signal is received
  5. Stop all actors
  6. Final cleanup and exit
Member

jdantonio commented Jul 27, 2015

EventMachine suggests that all concurrency be handled by EM, but that's not a requirement. EM just uses Ruby threads under-the-hood so you can mix-and-match. You just have to know what you are doing. The flow @pitr-ch suggests should work fine. The basic application flow would be:

  1. General initialization/configuration
  2. Start actors for processing received messages
  3. Start the EM reactor
    • Create I/O listeners
    • When a message is received, pass to appropriate actor for handling
  4. Stop the EM reactor when shutdown signal is received
  5. Stop all actors
  6. Final cleanup and exit

@jdantonio jdantonio closed this Aug 31, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment