88
99module Concurrent
1010
11- # {include:file:doc/async.md}
11+ # A mixin module that provides simple asynchronous behavior to any standard
12+ # class/object or object.
13+ #
14+ # ```cucumber
15+ # Feature:
16+ # As a stateful, plain old Ruby class/object
17+ # I want safe, asynchronous behavior
18+ # So my long-running methods don't block the main thread
19+ # ```
20+ #
21+ # Stateful, mutable objects must be managed carefully when used asynchronously.
22+ # But Ruby is an object-oriented language so designing with objects and classes
23+ # plays to Ruby's strengths and is often more natural to many Ruby programmers.
24+ # The `Async` module is a way to mix simple yet powerful asynchronous capabilities
25+ # into any plain old Ruby object or class. These capabilities provide a reasonable
26+ # level of thread safe guarantees when used correctly.
27+ #
28+ # When this module is mixed into a class or object it provides to new methods:
29+ # `async` and `await`. These methods are thread safe with respect to the enclosing
30+ # object. The former method allows methods to be called asynchronously by posting
31+ # to the global thread pool. The latter allows a method to be called synchronously
32+ # on the current thread but does so safely with respect to any pending asynchronous
33+ # method calls. Both methods return an `Obligation` which can be inspected for
34+ # the result of the method call. Calling a method with `async` will return a
35+ # `:pending` `Obligation` whereas `await` will return a `:complete` `Obligation`.
36+ #
37+ # Very loosely based on the `async` and `await` keywords in C#.
38+ #
39+ # ### An Important Note About Initialization
40+ #
41+ # > This module depends on several internal stnchronization mechanisms that
42+ # > must be initialized prior to calling any of the async/await/executor methods.
43+ # > To ensure thread-safe initialization the class `new` method will be made
44+ # > private when the `Concurrent::Async` module is included. A factory method
45+ # > called `create` will be defined in its place. The `create`factory will
46+ # > create a new object instance, passing all arguments to the constructor,
47+ # > and will initialize all stnchronization mechanisms. This is the only way
48+ # > thread-safe initialization can be guaranteed.
49+ #
50+ # ### An Important Note About Thread Safe Guarantees
51+ #
52+ # > Thread safe guarantees can only be made when asynchronous method calls
53+ # > are not mixed with synchronous method calls. Use only synchronous calls
54+ # > when the object is used exclusively on a single thread. Use only
55+ # > `async` and `await` when the object is shared between threads. Once you
56+ # > call a method using `async`, you should no longer call any methods
57+ # > directly on the object. Use `async` and `await` exclusively from then on.
58+ # > With careful programming it is possible to switch back and forth but it's
59+ # > also very easy to create race conditions and break your application.
60+ # > Basically, it's "async all the way down."
61+ #
62+ # @example
63+ #
64+ # class Echo
65+ # include Concurrent::Async
66+ #
67+ # def echo(msg)
68+ # sleep(rand)
69+ # print "#{msg}\n"
70+ # nil
71+ # end
72+ # end
73+ #
74+ # horn = Echo.new #=> NoMethodError: private method `new' called for Echo:Class
75+ #
76+ # horn = Echo.create
77+ # horn.echo('zero') # synchronous, not thread-safe
78+ #
79+ # horn.async.echo('one') # asynchronous, non-blocking, thread-safe
80+ # horn.await.echo('two') # synchronous, blocking, thread-safe
1281 #
1382 # @see Concurrent::Obligation
83+ # @see Concurrent::IVar
1484 module Async
1585
86+ # @!method self.create(*args, &block)
87+ #
88+ # The factory method used to create new instances of the asynchronous
89+ # class. Used instead of `new` to ensure proper initialization of the
90+ # synchronization mechanisms.
91+ #
92+ # @param [Array<Object>] args Zero or more arguments to be passed to the
93+ # object's initializer.
94+ # @param [Proc] bloc Optional block to pass to the object's initializer.
95+ # @return [Object] A properly initialized object of the asynchronous class.
96+
1697 # Check for the presence of a method on an object and determine if a given
1798 # set of arguments matches the required arity.
1899 #
@@ -32,7 +113,9 @@ module Async
32113 # @see http://www.ruby-doc.org/core-2.1.1/Method.html#method-i-arity Method#arity
33114 # @see http://ruby-doc.org/core-2.1.0/Object.html#method-i-respond_to-3F Object#respond_to?
34115 # @see http://www.ruby-doc.org/core-2.1.0/BasicObject.html#method-i-method_missing BasicObject#method_missing
35- def validate_argc ( obj , method , *args )
116+ #
117+ # @!visibility private
118+ def self . validate_argc ( obj , method , *args )
36119 argc = args . length
37120 arity = obj . method ( method ) . arity
38121
@@ -42,12 +125,35 @@ def validate_argc(obj, method, *args)
42125 raise ArgumentError . new ( "wrong number of arguments (#{ argc } for #{ arity } ..*)" )
43126 end
44127 end
45- module_function :validate_argc
128+
129+ # @!visibility private
130+ def self . included ( base )
131+ base . extend ( ClassMethods )
132+ base . send ( :private_class_method , :new )
133+ super ( base )
134+ end
135+
136+ # @!visibility private
137+ def self . extended ( base )
138+ base . extend ( ClassMethods )
139+ base . send ( :private_class_method , :new )
140+ super ( base )
141+ end
142+
143+ # @!visibility private
144+ module ClassMethods
145+ def create ( *args , &block )
146+ obj = self . send ( :new , *args , &block )
147+ obj . send ( :init_synchronization )
148+ obj
149+ end
150+ end
151+ private_constant :ClassMethods
46152
47153 # Delegates asynchronous, thread-safe method calls to the wrapped object.
48154 #
49155 # @!visibility private
50- class AsyncDelegator # :nodoc:
156+ class AsyncDelegator
51157
52158 # Create a new delegator object wrapping the given delegate,
53159 # protecting it with the given serializer, and executing it on the
@@ -96,6 +202,7 @@ def method_missing(method, *args, &block)
96202 self . send ( method , *args )
97203 end
98204 end
205+ private_constant :AsyncDelegator
99206
100207 # Causes the chained method call to be performed asynchronously on the
101208 # global thread pool. The method called by this method will return a
@@ -110,26 +217,24 @@ def method_missing(method, *args, &block)
110217 # library, some edge cases will be missed. For more information see
111218 # the documentation for the `validate_argc` method.
112219 #
113- # @note The method call is guaranteed to be thread safe with respect to
114- # all other method calls against the same object that are called with
115- # either `async` or `await`. The mutable nature of Ruby references
116- # (and object orientation in general) prevent any other thread safety
117- # guarantees. Do NOT mix non-protected method calls with protected
118- # method call. Use *only* protected method calls when sharing the object
119- # between threads.
220+ # @!macro [attach] async_thread_safety_warning
221+ # @note The method call is guaranteed to be thread safe with respect to
222+ # all other method calls against the same object that are called with
223+ # either `async` or `await`. The mutable nature of Ruby references
224+ # (and object orientation in general) prevent any other thread safety
225+ # guarantees. Do NOT mix non-protected method calls with protected
226+ # method call. Use *only* protected method calls when sharing the object
227+ # between threads.
120228 #
121229 # @return [Concurrent::IVar] the pending result of the asynchronous operation
122230 #
123- # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
124231 # @raise [NameError] the object does not respond to `method` method
125232 # @raise [ArgumentError] the given `args` do not match the arity of `method`
126233 #
127234 # @see Concurrent::IVar
128235 def async
129- raise InitializationError . new ( '#init_mutex was never called' ) unless @__async_initialized__
130236 @__async_delegator__ . value
131237 end
132- alias_method :future , :async
133238
134239 # Causes the chained method call to be performed synchronously on the
135240 # current thread. The method called by this method will return an
@@ -144,51 +249,38 @@ def async
144249 # library, some edge cases will be missed. For more information see
145250 # the documentation for the `validate_argc` method.
146251 #
147- # @note The method call is guaranteed to be thread safe with respect to
148- # all other method calls against the same object that are called with
149- # either `async` or `await`. The mutable nature of Ruby references
150- # (and object orientation in general) prevent any other thread safety
151- # guarantees. Do NOT mix non-protected method calls with protected
152- # method call. Use *only* protected method calls when sharing the object
153- # between threads.
252+ # @!macro async_thread_safety_warning
154253 #
155254 # @return [Concurrent::IVar] the completed result of the synchronous operation
156255 #
157- # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
158256 # @raise [NameError] the object does not respond to `method` method
159257 # @raise [ArgumentError] the given `args` do not match the arity of `method`
160258 #
161259 # @see Concurrent::IVar
162260 def await
163- raise InitializationError . new ( '#init_mutex was never called' ) unless @__async_initialized__
164261 @__await_delegator__ . value
165262 end
166- alias_method :delay , :await
167263
168- # Set a new executor
264+ # Set a new executor.
169265 #
170- # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
171- # @raise [ArgumentError] executor has already been set
266+ # @raise [ArgumentError] executor has already been set.
172267 def executor = ( executor )
173- raise InitializationError . new ( '#init_mutex was never called' ) unless @__async_initialized__
174268 @__async_executor__ . reconfigure { executor } or
175269 raise ArgumentError . new ( 'executor has already been set' )
176270 end
177271
178- # Initialize the internal serializer and other synchronization objects. This method
179- # *must* be called from the constructor of the including class or explicitly
180- # by the caller prior to calling any other methods. If `init_mutex` is *not*
181- # called explicitly the async/await/executor methods will raize a
182- # `Concurrent::InitializationError`. This is the only way thread-safe
183- # initialization can be guaranteed.
272+ private
273+
274+ # Initialize the internal serializer and other stnchronization mechanisms.
184275 #
185- # @note This method *must* be called from the constructor of the including
186- # class or explicitly by the caller prior to calling any other methods.
187- # This is the only way thread-safe initialization can be guaranteed.
276+ # @note This method *must* be called immediately upon object construction.
277+ # This is the only way thread-safe initialization can be guaranteed.
188278 #
189279 # @raise [Concurrent::InitializationError] when called more than once
190- def init_mutex
191- raise InitializationError . new ( '#init_mutex was already called' ) if @__async_initialized__
280+ #
281+ # @!visibility private
282+ def init_synchronization
283+ raise InitializationError . new ( '#init_synchronization was already called' ) if @__async_initialized__
192284
193285 @__async_initialized__ = true
194286 serializer = Concurrent ::SerializedExecution . new
@@ -204,6 +296,8 @@ def init_mutex
204296 @__async_delegator__ = Delay . new {
205297 AsyncDelegator . new ( self , @__async_executor__ , serializer , false )
206298 }
299+
300+ self
207301 end
208302 end
209303end
0 commit comments