|
| 1 | +A mixin module that provides simple asynchronous behavior to any standard |
| 2 | +class/object or object. |
| 3 | + |
| 4 | +```cucumber |
| 5 | +Feature: |
| 6 | + As a stateful, plain old Ruby class/object |
| 7 | + I want safe, asynchronous behavior |
| 8 | + So my long-running methods don't block the main thread |
| 9 | +``` |
| 10 | + |
| 11 | +Stateful, mutable objects must be managed carefully when used asynchronously. |
| 12 | +But Ruby is an object-oriented language so designing with objects and classes |
| 13 | +plays to Ruby's strengths and is often more natural to many Ruby programmers. |
| 14 | +The `Async` module is a way to mix simple yet powerful asynchronous capabilities |
| 15 | +into any plain old Ruby object or class. These capabilities provide a reasonable |
| 16 | +level of thread safe guarantees when used correctly. |
| 17 | + |
| 18 | +When this module is mixed into a class or object it provides to new methods: |
| 19 | +`async` and `await`. These methods are thread safe with respect to the enclosing |
| 20 | +object. The former method allows methods to be called asynchronously by posting |
| 21 | +to the global thread pool. The latter allows a method to be called synchronously |
| 22 | +on the current thread but does so safely with respect to any pending asynchronous |
| 23 | +method calls. Both methods return an `Obligation` which can be inspected for |
| 24 | +the result of the method call. Calling a method with `async` will return a |
| 25 | +`:pending` `Obligation` whereas `await` will return a `:complete` `Obligation`. |
| 26 | + |
| 27 | +Very loosely based on the `async` and `await` keywords in C#. |
| 28 | + |
| 29 | +#### An Important Note About Initialization |
| 30 | + |
| 31 | +> This module depends on several internal synchronization objects that |
| 32 | +> must be initialized prior to calling any of the async/await/executor methods. |
| 33 | +> The best practice is to call `init_mutex` from within the constructor |
| 34 | +> of the including class. A less ideal but acceptable practice is for the |
| 35 | +> thread creating the asynchronous object to explicitly call the `init_mutex` |
| 36 | +> method prior to calling any of the async/await/executor methods. If |
| 37 | +> `init_mutex` is *not* called explicitly the async/await/executor methods |
| 38 | +> will raise a `Concurrent::InitializationError`. This is the only way |
| 39 | +> thread-safe initialization can be guaranteed. |
| 40 | +
|
| 41 | +#### An Important Note About Thread Safe Guarantees |
| 42 | + |
| 43 | +> Thread safe guarantees can only be made when asynchronous method calls |
| 44 | +> are not mixed with synchronous method calls. Use only synchronous calls |
| 45 | +> when the object is used exclusively on a single thread. Use only |
| 46 | +> `async` and `await` when the object is shared between threads. Once you |
| 47 | +> call a method using `async`, you should no longer call any methods |
| 48 | +> directly on the object. Use `async` and `await` exclusively from then on. |
| 49 | +> With careful programming it is possible to switch back and forth but it's |
| 50 | +> also very easy to create race conditions and break your application. |
| 51 | +> Basically, it's "async all the way down." |
| 52 | +
|
| 53 | +### Examples |
| 54 | + |
| 55 | +#### Defining an asynchronous class |
| 56 | + |
| 57 | +```cucumber |
| 58 | +Scenario: Defining an asynchronous class |
| 59 | + Given a class definition |
| 60 | + When I include the Concurrent::Async module |
| 61 | + Then an `async` method is defined for all objects of the class |
| 62 | + And an `await` method is defined for all objects of the class |
| 63 | +
|
| 64 | +Scenario: Calling the `async` method |
| 65 | + Given a class which includes Concurrent::Async module |
| 66 | + When I call a normal method through the `async` delegate method |
| 67 | + Then the method returns a Concurrent::Future object |
| 68 | + And the method is executed on a background thread using the global thread pool |
| 69 | + And the current thread does not block |
| 70 | + And thread safety is provided with respect to other `async` and `await` calls |
| 71 | + And the returned future can be interrogated for the status of the method call |
| 72 | + And the returned future will eventually contain the `value` of the method call |
| 73 | + Or the returned future will eventually contain the `reason` the method call failed |
| 74 | +
|
| 75 | +Scenario: Calling the `await` method |
| 76 | + Given a class which includes Concurrent::Async module |
| 77 | + When I call a normal method through the `await` delegate method |
| 78 | + Then the method returns a Concurrent::IVar object |
| 79 | + And the method is executed on the current thread |
| 80 | + And thread safety is provided with respect to other `async` and `await` calls |
| 81 | + And the returned ivar will be in the :fulfilled state |
| 82 | + Or the returned ivar will be in the :rejected state |
| 83 | + And the returned ivar will immediately contain the `value` of the method call |
| 84 | + Or the returned ivar will immediately contain the `reason` the method call failed |
| 85 | +``` |
| 86 | + |
| 87 | +```ruby |
| 88 | +class Echo |
| 89 | + include Concurrent::Async |
| 90 | + |
| 91 | + def initialize |
| 92 | + init_mutex # initialize the internal synchronization objects |
| 93 | + end |
| 94 | + |
| 95 | + def echo(msg) |
| 96 | + sleep(rand) |
| 97 | + print "#{msg}\n" |
| 98 | + nil |
| 99 | + end |
| 100 | +end |
| 101 | + |
| 102 | +horn = Echo.new |
| 103 | +horn.echo('zero') # synchronous, not thread-safe |
| 104 | + |
| 105 | +horn.async.echo('one') # asynchronous, non-blocking, thread-safe |
| 106 | +horn.await.echo('two') # synchronous, blocking, thread-safe |
| 107 | +``` |
| 108 | + |
| 109 | +#### Monkey-patching an existing object |
| 110 | + |
| 111 | +```cucumber |
| 112 | +Scenario: Monkey-patching an existing object |
| 113 | + Given an object of a class that does not include the Concurrent::Async module |
| 114 | + When I extend the object with the Concurrent::Async module |
| 115 | + Then an `async` method is monkey-patched onto the object |
| 116 | + And an `await` method is monkey-patched onto the object |
| 117 | + And the object behaved as though Concurrent::Async were included in the class |
| 118 | + And the `async` and `await` methods perform as expected |
| 119 | + And no other objects of that class are affected |
| 120 | +``` |
| 121 | + |
| 122 | +```ruby |
| 123 | +numbers = 1_000_000.times.collect{ rand } |
| 124 | +numbers.extend(Concurrent::Async) |
| 125 | +numbers.init_mutex # initialize the internal synchronization objects |
| 126 | + |
| 127 | +future = numbers.async.max |
| 128 | +future.state #=> :pending |
| 129 | + |
| 130 | +sleep(2) |
| 131 | + |
| 132 | +future.state #=> :fulfilled |
| 133 | +future.value #=> 0.999999138918843 |
| 134 | +``` |
0 commit comments