8
8
9
9
module Concurrent
10
10
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
12
81
#
13
82
# @see Concurrent::Obligation
83
+ # @see Concurrent::IVar
14
84
module Async
15
85
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
+
16
97
# Check for the presence of a method on an object and determine if a given
17
98
# set of arguments matches the required arity.
18
99
#
@@ -32,7 +113,9 @@ module Async
32
113
# @see http://www.ruby-doc.org/core-2.1.1/Method.html#method-i-arity Method#arity
33
114
# @see http://ruby-doc.org/core-2.1.0/Object.html#method-i-respond_to-3F Object#respond_to?
34
115
# @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 )
36
119
argc = args . length
37
120
arity = obj . method ( method ) . arity
38
121
@@ -42,12 +125,35 @@ def validate_argc(obj, method, *args)
42
125
raise ArgumentError . new ( "wrong number of arguments (#{ argc } for #{ arity } ..*)" )
43
126
end
44
127
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
46
152
47
153
# Delegates asynchronous, thread-safe method calls to the wrapped object.
48
154
#
49
155
# @!visibility private
50
- class AsyncDelegator # :nodoc:
156
+ class AsyncDelegator
51
157
52
158
# Create a new delegator object wrapping the given delegate,
53
159
# protecting it with the given serializer, and executing it on the
@@ -96,6 +202,7 @@ def method_missing(method, *args, &block)
96
202
self . send ( method , *args )
97
203
end
98
204
end
205
+ private_constant :AsyncDelegator
99
206
100
207
# Causes the chained method call to be performed asynchronously on the
101
208
# global thread pool. The method called by this method will return a
@@ -110,26 +217,24 @@ def method_missing(method, *args, &block)
110
217
# library, some edge cases will be missed. For more information see
111
218
# the documentation for the `validate_argc` method.
112
219
#
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.
120
228
#
121
229
# @return [Concurrent::IVar] the pending result of the asynchronous operation
122
230
#
123
- # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
124
231
# @raise [NameError] the object does not respond to `method` method
125
232
# @raise [ArgumentError] the given `args` do not match the arity of `method`
126
233
#
127
234
# @see Concurrent::IVar
128
235
def async
129
- raise InitializationError . new ( '#init_mutex was never called' ) unless @__async_initialized__
130
236
@__async_delegator__ . value
131
237
end
132
- alias_method :future , :async
133
238
134
239
# Causes the chained method call to be performed synchronously on the
135
240
# current thread. The method called by this method will return an
@@ -144,51 +249,38 @@ def async
144
249
# library, some edge cases will be missed. For more information see
145
250
# the documentation for the `validate_argc` method.
146
251
#
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
154
253
#
155
254
# @return [Concurrent::IVar] the completed result of the synchronous operation
156
255
#
157
- # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
158
256
# @raise [NameError] the object does not respond to `method` method
159
257
# @raise [ArgumentError] the given `args` do not match the arity of `method`
160
258
#
161
259
# @see Concurrent::IVar
162
260
def await
163
- raise InitializationError . new ( '#init_mutex was never called' ) unless @__async_initialized__
164
261
@__await_delegator__ . value
165
262
end
166
- alias_method :delay , :await
167
263
168
- # Set a new executor
264
+ # Set a new executor.
169
265
#
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.
172
267
def executor = ( executor )
173
- raise InitializationError . new ( '#init_mutex was never called' ) unless @__async_initialized__
174
268
@__async_executor__ . reconfigure { executor } or
175
269
raise ArgumentError . new ( 'executor has already been set' )
176
270
end
177
271
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.
184
275
#
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.
188
278
#
189
279
# @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__
192
284
193
285
@__async_initialized__ = true
194
286
serializer = Concurrent ::SerializedExecution . new
@@ -204,6 +296,8 @@ def init_mutex
204
296
@__async_delegator__ = Delay . new {
205
297
AsyncDelegator . new ( self , @__async_executor__ , serializer , false )
206
298
}
299
+
300
+ self
207
301
end
208
302
end
209
303
end
0 commit comments