Skip to content

Commit 8392ddb

Browse files
committed
Async now hides :new and provides safe :create factory.
1 parent 1cdd308 commit 8392ddb

File tree

3 files changed

+184
-221
lines changed

3 files changed

+184
-221
lines changed

doc/async.md

Lines changed: 0 additions & 134 deletions
This file was deleted.

lib/concurrent/async.rb

Lines changed: 133 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,92 @@
88

99
module 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
209303
end

0 commit comments

Comments
 (0)