1
1
require 'thread'
2
2
require 'concurrent/configuration'
3
+ require 'concurrent/delay'
3
4
require 'concurrent/ivar'
4
5
require 'concurrent/future'
5
6
require 'concurrent/executor/thread_pool_executor'
6
7
7
8
module Concurrent
8
9
10
+ InitializationError = Class . new ( StandardError )
11
+
9
12
# A mixin module that provides simple asynchronous behavior to any standard
10
13
# class/object or object.
11
14
#
@@ -36,6 +39,10 @@ module Concurrent
36
39
# class Echo
37
40
# include Concurrent::Async
38
41
#
42
+ # def initialize
43
+ # init_mutex # initialize the internal synchronization objects
44
+ # end
45
+ #
39
46
# def echo(msg)
40
47
# sleep(rand)
41
48
# print "#{msg}\n"
@@ -52,6 +59,7 @@ module Concurrent
52
59
# @example Monkey-patching an existing object
53
60
# numbers = 1_000_000.times.collect{ rand }
54
61
# numbers.extend(Concurrent::Async)
62
+ # numbers.init_mutex # initialize the internal synchronization objects
55
63
#
56
64
# future = numbers.async.max
57
65
# future.state #=> :pending
@@ -61,6 +69,16 @@ module Concurrent
61
69
# future.state #=> :fulfilled
62
70
# future.value #=> 0.999999138918843
63
71
#
72
+ # @note This module depends on several internal synchronization objects that
73
+ # must be initialized prior to calling any of the async/await/executor methods.
74
+ # The best practice is to call `init_mutex` from within the constructor
75
+ # of the including class. A less ideal but acceptable practice is for the
76
+ # thread creating the asynchronous object to explicitly call the `init_mutex`
77
+ # method prior to calling any of the async/await/executor methods. If
78
+ # `init_mutex` is *not* called explicitly the async/await/executor methods
79
+ # will raize a `Concurrent::InitializationError`. This is the only way
80
+ # thread-safe initialization can be guaranteed.
81
+ #
64
82
# @note Thread safe guarantees can only be made when asynchronous method calls
65
83
# are not mixed with synchronous method calls. Use only synchronous calls
66
84
# when the object is used exclusively on a single thread. Use only
@@ -142,7 +160,7 @@ def method_missing(method, *args, &block)
142
160
ivar = Concurrent ::IVar . new
143
161
value , reason = nil , nil
144
162
begin
145
- mutex . synchronize do
163
+ @ mutex. synchronize do
146
164
value = @delegate . send ( method , *args , &block )
147
165
end
148
166
rescue => reason
@@ -154,11 +172,6 @@ def method_missing(method, *args, &block)
154
172
155
173
self . send ( method , *args )
156
174
end
157
-
158
- # The lock used when delegating methods to the wrapped object.
159
- #
160
- # @!visibility private
161
- attr_reader :mutex # :nodoc:
162
175
end
163
176
164
177
# Delegates asynchronous, thread-safe method calls to the wrapped object.
@@ -194,22 +207,15 @@ def method_missing(method, *args, &block)
194
207
195
208
self . define_singleton_method ( method ) do |*args |
196
209
Async ::validate_argc ( @delegate , method , *args )
197
- Concurrent ::Future . execute ( executor : @executor ) do
198
- mutex . synchronize do
210
+ Concurrent ::Future . execute ( executor : @executor . value ) do
211
+ @ mutex. synchronize do
199
212
@delegate . send ( method , *args , &block )
200
213
end
201
214
end
202
215
end
203
216
204
217
self . send ( method , *args )
205
218
end
206
-
207
- private
208
-
209
- # The lock used when delegating methods to the wrapped object.
210
- #
211
- # @!visibility private
212
- attr_reader :mutex # :nodoc:
213
219
end
214
220
215
221
# Causes the chained method call to be performed asynchronously on the
@@ -230,17 +236,19 @@ def method_missing(method, *args, &block)
230
236
# either `async` or `await`. The mutable nature of Ruby references
231
237
# (and object orientation in general) prevent any other thread safety
232
238
# guarantees. Do NOT mix non-protected method calls with protected
233
- # method call. Use ONLY protected method calls when sharing the object
239
+ # method call. Use *only* protected method calls when sharing the object
234
240
# between threads.
235
241
#
236
242
# @return [Concurrent::Future] the pending result of the asynchronous operation
237
243
#
244
+ # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
238
245
# @raise [NameError] the object does not respond to `method` method
239
246
# @raise [ArgumentError] the given `args` do not match the arity of `method`
240
247
#
241
248
# @see Concurrent::Future
242
249
def async
243
- @__async_delegator__ ||= AsyncDelegator . new ( self , executor , await . mutex )
250
+ raise InitializationError . new ( '#init_mutex was never called' ) unless @mutex
251
+ @__async_delegator__ . value
244
252
end
245
253
alias_method :future , :async
246
254
@@ -262,29 +270,51 @@ def async
262
270
# either `async` or `await`. The mutable nature of Ruby references
263
271
# (and object orientation in general) prevent any other thread safety
264
272
# guarantees. Do NOT mix non-protected method calls with protected
265
- # method call. Use ONLY protected method calls when sharing the object
273
+ # method call. Use *only* protected method calls when sharing the object
266
274
# between threads.
267
275
#
268
276
# @return [Concurrent::IVar] the completed result of the synchronous operation
269
277
#
278
+ # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
270
279
# @raise [NameError] the object does not respond to `method` method
271
280
# @raise [ArgumentError] the given `args` do not match the arity of `method`
272
281
#
273
282
# @see Concurrent::IVar
274
283
def await
275
- @__await_delegator__ ||= AwaitDelegator . new ( self , Mutex . new )
284
+ raise InitializationError . new ( '#init_mutex was never called' ) unless @mutex
285
+ @__await_delegator__ . value
276
286
end
277
287
alias_method :delay , :await
278
288
289
+ # Set a new executor
290
+ #
291
+ # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
292
+ # @raise [ArgumentError] executor has already been set
279
293
def executor = ( executor )
280
- raise ArgumentError . new ( 'executor has already been set' ) unless @__async__executor__ . nil?
281
- @__async__executor__ = executor
294
+ raise InitializationError . new ( '#init_mutex was never called' ) unless @mutex
295
+ @__async__executor__ . reconfigure { executor } or
296
+ raise ArgumentError . new ( 'executor has already been set' )
282
297
end
283
298
284
- private
285
-
286
- def executor
287
- @__async__executor__ ||= Concurrent . configuration . global_task_pool
299
+ # Initialize the internal mutex and other synchronization objects. This method
300
+ # *must* be called from the constructor of the including class or explicitly
301
+ # by the caller prior to calling any other methods. If `init_mutex` is *not*
302
+ # called explicitly the async/await/executor methods will raize a
303
+ # `Concurrent::InitializationError`. This is the only way thread-safe
304
+ # initialization can be guaranteed.
305
+ #
306
+ # @note This method *must* be called from the constructor of the including
307
+ # class or explicitly by the caller prior to calling any other methods.
308
+ # This is the only way thread-safe initialization can be guaranteed.
309
+ #
310
+ # @raise [Concurrent::InitializationError] when called more than once
311
+ def init_mutex
312
+ raise InitializationError . new ( '#init_mutex was already called' ) if @mutex
313
+ ( @mutex = Mutex . new ) . lock
314
+ @__async__executor__ = Delay . new { Concurrent . configuration . global_operation_pool }
315
+ @__await_delegator__ = Delay . new { AwaitDelegator . new ( self , @mutex ) }
316
+ @__async_delegator__ = Delay . new { AsyncDelegator . new ( self , @__async__executor__ , @mutex ) }
317
+ @mutex . unlock
288
318
end
289
319
end
290
320
end
0 commit comments