ractor-sharing 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +261 -0
- data/docs/active_object.md +211 -0
- data/docs/actor_hash.md +144 -0
- data/docs/lockhash.md +150 -0
- data/docs/lockvar.md +280 -0
- data/docs/tvar.md +124 -0
- data/ext/ractor/lock/extconf.rb +2 -0
- data/ext/ractor/lock/lock.c +288 -0
- data/ext/ractor/lock/lock.h +59 -0
- data/ext/ractor/lock/lockhash.c +407 -0
- data/ext/ractor/lock/lockvar.c +259 -0
- data/ext/ractor/tvar/extconf.rb +2 -0
- data/ext/ractor/tvar/tvar.c +869 -0
- data/lib/ractor/active_object/future.rb +58 -0
- data/lib/ractor/active_object.rb +311 -0
- data/lib/ractor/actor_hash.rb +98 -0
- data/lib/ractor/lockhash.rb +3 -0
- data/lib/ractor/lockvar.rb +3 -0
- data/lib/ractor/sharing/version.rb +7 -0
- data/lib/ractor/sharing.rb +30 -0
- data/lib/ractor/tvar.rb +13 -0
- metadata +66 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Ractor
|
|
4
|
+
class ActiveObject
|
|
5
|
+
# Result handle returned by +future+ invocations.
|
|
6
|
+
#
|
|
7
|
+
# Only the Ractor that issued the invocation may call #value / #wait,
|
|
8
|
+
# because the underlying reply port belongs to that Ractor.
|
|
9
|
+
class Future
|
|
10
|
+
def initialize(port)
|
|
11
|
+
@port = port
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@state = :pending # :pending | :fulfilled | :rejected
|
|
14
|
+
@result = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Pre-resolved futures, used when the caller is already the owner.
|
|
18
|
+
def self.__ao_fulfilled__(value) = new(nil).tap { |f| f.__ao_settle__(:fulfilled, value) }
|
|
19
|
+
def self.__ao_rejected__(exc) = new(nil).tap { |f| f.__ao_settle__(:rejected, exc) }
|
|
20
|
+
|
|
21
|
+
# Waits for completion. Returns self; never raises for a rejected future.
|
|
22
|
+
def wait
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
if @state == :pending
|
|
25
|
+
status, payload, backtrace = @port.receive
|
|
26
|
+
@port = nil
|
|
27
|
+
if status == :ok
|
|
28
|
+
__ao_settle__(:fulfilled, payload)
|
|
29
|
+
else
|
|
30
|
+
__ao_settle__(:rejected, ActiveObject.__ao_restore_exception__(payload, backtrace))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Waits for completion and returns the method's return value, or
|
|
38
|
+
# re-raises the exception the method raised on the owner Ractor.
|
|
39
|
+
def value
|
|
40
|
+
wait
|
|
41
|
+
raise @result if @state == :rejected
|
|
42
|
+
@result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# True once #wait / #value has observed the result. Does not poll.
|
|
46
|
+
def resolved? = @state != :pending
|
|
47
|
+
|
|
48
|
+
def rejected? = @state == :rejected
|
|
49
|
+
|
|
50
|
+
def __ao_settle__(state, result)
|
|
51
|
+
@state = state
|
|
52
|
+
@result = result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def inspect = "#<#{self.class} #{@state}>"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "active_object/future"
|
|
4
|
+
|
|
5
|
+
class Ractor
|
|
6
|
+
# Active Object pattern on Ractors: each instance's state lives in its own
|
|
7
|
+
# owner Ractor. `Foo.new` returns a proxy that publishes only the methods
|
|
8
|
+
# declared with sync/async/future; they are executed on the owner one at a
|
|
9
|
+
# time. See README.md.
|
|
10
|
+
class ActiveObject
|
|
11
|
+
class Error < ::Ractor::Error; end
|
|
12
|
+
|
|
13
|
+
POLICIES = %i[sync async future].freeze
|
|
14
|
+
|
|
15
|
+
DEF_NAME = /\A(?:[[:alpha:]_][[:alnum:]_]*[?!=]?|[+\-*\/%&|^<>~!`]|\*\*|[=!]=|<=|>=|<=>|===|=~|!~|<<|>>|\[\]=?|[+\-]@)\z/
|
|
16
|
+
private_constant :DEF_NAME
|
|
17
|
+
|
|
18
|
+
# Shared by proxies and servants: explicit *_send and the remote protocol.
|
|
19
|
+
module Dispatch
|
|
20
|
+
# The Ractor that owns the object's state.
|
|
21
|
+
def owner = @__ao_owner__
|
|
22
|
+
|
|
23
|
+
# True when called from the owner Ractor.
|
|
24
|
+
def owner? = Ractor.current.equal?(@__ao_owner__)
|
|
25
|
+
|
|
26
|
+
def sync_send(name, *args, **kwargs, &block) = __ao_call__(:sync, name.to_sym, args, kwargs, block)
|
|
27
|
+
def async_send(name, *args, **kwargs, &block) = __ao_call__(:async, name.to_sym, args, kwargs, block)
|
|
28
|
+
def future_send(name, *args, **kwargs, &block) = __ao_call__(:future, name.to_sym, args, kwargs, block)
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def __ao_call__(policy, name, args, kwargs, block)
|
|
33
|
+
return __ao_remote__(policy, name, args, kwargs, block) unless Ractor.current.equal?(@__ao_owner__)
|
|
34
|
+
|
|
35
|
+
servant = Ractor[:__ao_servant__]
|
|
36
|
+
case policy
|
|
37
|
+
when :sync then servant.__send__(name, *args, **kwargs, &block)
|
|
38
|
+
when :async then servant.__send__(name, *args, **kwargs, &block); nil
|
|
39
|
+
when :future
|
|
40
|
+
begin
|
|
41
|
+
Future.__ao_fulfilled__(servant.__send__(name, *args, **kwargs, &block))
|
|
42
|
+
rescue Exception => e
|
|
43
|
+
Future.__ao_rejected__(e)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def __ao_remote__(policy, name, args, kwargs, block)
|
|
49
|
+
port = @__ao_port__
|
|
50
|
+
raise Error, "#{self.class} instance was not created by .new (no owner Ractor)" unless port
|
|
51
|
+
raise ArgumentError, "block is not supported for remote invocation (#{name})" if block
|
|
52
|
+
|
|
53
|
+
case policy
|
|
54
|
+
when :sync
|
|
55
|
+
status, payload, backtrace = ActiveObject.__ao_sync_call__ do |reply|
|
|
56
|
+
__ao_post__(port, [:sync, reply, name, args, kwargs])
|
|
57
|
+
end
|
|
58
|
+
raise ActiveObject.__ao_restore_exception__(payload, backtrace) if status == :raise
|
|
59
|
+
payload
|
|
60
|
+
when :async
|
|
61
|
+
__ao_post__(port, [:async, nil, name, args, kwargs])
|
|
62
|
+
nil
|
|
63
|
+
when :future
|
|
64
|
+
reply = Ractor::Port.new
|
|
65
|
+
__ao_post__(port, [:future, reply, name, args, kwargs])
|
|
66
|
+
Future.new(reply)
|
|
67
|
+
else
|
|
68
|
+
raise ArgumentError, "unknown invocation policy: #{policy.inspect}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def __ao_post__(port, request)
|
|
73
|
+
port << request
|
|
74
|
+
rescue Ractor::ClosedError
|
|
75
|
+
raise Error, "owner Ractor #{@__ao_owner__.inspect} terminated"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Base of the per-class proxy classes (Foo::Proxy). A proxy is frozen and
|
|
80
|
+
# shareable and has one method per declared (published) method of Foo.
|
|
81
|
+
class Proxy
|
|
82
|
+
include Dispatch
|
|
83
|
+
|
|
84
|
+
def initialize(owner, port)
|
|
85
|
+
@__ao_owner__ = owner
|
|
86
|
+
@__ao_port__ = port
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The Ractor::ActiveObject subclass this proxy stands for.
|
|
90
|
+
def active_object_class = self.class.active_object_class
|
|
91
|
+
|
|
92
|
+
def inspect = "#<#{self.class} owner=#{@__ao_owner__.inspect}>"
|
|
93
|
+
|
|
94
|
+
class << self
|
|
95
|
+
attr_reader :active_object_class
|
|
96
|
+
|
|
97
|
+
def __ao_bind__(klass)
|
|
98
|
+
@active_object_class = klass
|
|
99
|
+
klass.const_set(:Proxy, self) # named Foo::Proxy
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
include Dispatch
|
|
106
|
+
|
|
107
|
+
@__ao_policies__ = {}.freeze
|
|
108
|
+
@__ao_proxy_class__ = Proxy
|
|
109
|
+
|
|
110
|
+
class << self
|
|
111
|
+
# Creates the owner Ractor, runs +initialize+ there and returns a
|
|
112
|
+
# shareable proxy which forwards declared methods to the owner.
|
|
113
|
+
def new(*args, **kwargs, &block)
|
|
114
|
+
raise ArgumentError, "#{self}.new does not accept a block (it cannot be sent to the owner Ractor)" if block
|
|
115
|
+
|
|
116
|
+
boot = Ractor::Port.new
|
|
117
|
+
owner = Ractor.new(self, args, kwargs, boot, name: "ActiveObject(#{self})") do |klass, a, kw, b|
|
|
118
|
+
Ractor::ActiveObject.__ao_run__(klass, a, kw, b)
|
|
119
|
+
end
|
|
120
|
+
# Only here: the owner may die before it can answer (e.g. .allocate fails).
|
|
121
|
+
src, (status, payload, backtrace) = Ractor.select(boot, owner)
|
|
122
|
+
raise Error, "owner Ractor #{owner.inspect} terminated during initialization" if src.equal?(owner)
|
|
123
|
+
raise __ao_restore_exception__(payload, backtrace) if status == :raise
|
|
124
|
+
|
|
125
|
+
Ractor.make_shareable(proxy_class.new(owner, payload))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Class of the proxies returned by .new (a subclass of ActiveObject::Proxy).
|
|
129
|
+
def proxy_class = @__ao_proxy_class__
|
|
130
|
+
|
|
131
|
+
# --- policy DSL -------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
# Publish already defined methods on the proxy with the given policy:
|
|
134
|
+
# sync def foo ... end
|
|
135
|
+
# async :bar, :baz
|
|
136
|
+
def sync(*names) = __ao_declare__(:sync, names)
|
|
137
|
+
def async(*names) = __ao_declare__(:async, names)
|
|
138
|
+
def future(*names) = __ao_declare__(:future, names)
|
|
139
|
+
|
|
140
|
+
# Policy of +name+ (:sync, :async or :future), or nil if not published.
|
|
141
|
+
def invocation_policy(name)
|
|
142
|
+
name = name.to_sym
|
|
143
|
+
klass = self
|
|
144
|
+
while klass
|
|
145
|
+
table = klass.instance_variable_get(:@__ao_policies__)
|
|
146
|
+
return table[name] if table&.key?(name)
|
|
147
|
+
klass = klass.superclass
|
|
148
|
+
end
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def inherited(subclass)
|
|
153
|
+
super
|
|
154
|
+
subclass.instance_variable_set(:@__ao_policies__, {}.freeze)
|
|
155
|
+
subclass.instance_variable_set(:@__ao_proxy_class__, Class.new(proxy_class).__ao_bind__(subclass))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# --- internals --------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def __ao_declare__(policy, names)
|
|
161
|
+
raise ArgumentError, "#{policy} requires method name(s), e.g. `#{policy} def foo; end`" if names.empty?
|
|
162
|
+
raise TypeError, "#{self} is not a subclass of Ractor::ActiveObject" if equal?(Ractor::ActiveObject)
|
|
163
|
+
|
|
164
|
+
names = names.flatten.map(&:to_sym)
|
|
165
|
+
names.each do |n|
|
|
166
|
+
unless method_defined?(n) || private_method_defined?(n)
|
|
167
|
+
raise NameError.new("undefined method '#{n}' for class '#{self}'", n)
|
|
168
|
+
end
|
|
169
|
+
# The table is read from any Ractor, so replace it by a shareable copy.
|
|
170
|
+
@__ao_policies__ = Ractor.make_shareable(@__ao_policies__.merge(n => policy))
|
|
171
|
+
__ao_define_proxy_method__(n, policy)
|
|
172
|
+
end
|
|
173
|
+
names.size == 1 ? names[0] : names
|
|
174
|
+
end
|
|
175
|
+
private :__ao_declare__
|
|
176
|
+
|
|
177
|
+
def __ao_define_proxy_method__(name, policy)
|
|
178
|
+
proxy = proxy_class
|
|
179
|
+
sym = name.inspect
|
|
180
|
+
# A string def has no closure, so the method is callable from any Ractor.
|
|
181
|
+
body = <<~RUBY
|
|
182
|
+
if Ractor.current.equal?(@__ao_owner__)
|
|
183
|
+
Ractor[:__ao_servant__].__send__(#{sym}, *args, **kwargs, &block)
|
|
184
|
+
else
|
|
185
|
+
__ao_remote__(#{policy.inspect}, #{sym}, args, kwargs, block)
|
|
186
|
+
end
|
|
187
|
+
RUBY
|
|
188
|
+
proxy.remove_method(name) if proxy.method_defined?(name, false)
|
|
189
|
+
if name.match?(DEF_NAME)
|
|
190
|
+
proxy.class_eval("def #{name}(*args, **kwargs, &block)\n#{body}end", __FILE__, __LINE__)
|
|
191
|
+
else
|
|
192
|
+
proxy.class_eval("define_method(#{sym}, &Ractor.shareable_proc { |*args, **kwargs, &block|\n#{body}})", __FILE__, __LINE__)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
private :__ao_define_proxy_method__
|
|
196
|
+
|
|
197
|
+
# Owner Ractor body: build the servant, run initialize, then serve requests.
|
|
198
|
+
def __ao_run__(klass, args, kwargs, boot)
|
|
199
|
+
port = Ractor::Port.new
|
|
200
|
+
servant = klass.allocate
|
|
201
|
+
servant.instance_variable_set(:@__ao_owner__, Ractor.current)
|
|
202
|
+
servant.instance_variable_set(:@__ao_port__, port)
|
|
203
|
+
Ractor[:__ao_servant__] = servant
|
|
204
|
+
|
|
205
|
+
begin
|
|
206
|
+
servant.__send__(:initialize, *args, **kwargs)
|
|
207
|
+
rescue Exception => e
|
|
208
|
+
__ao_reply__(boot, :raise, e)
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
boot << [:ok, port]
|
|
212
|
+
|
|
213
|
+
proxy = klass.proxy_class.new(Ractor.current, port).freeze
|
|
214
|
+
pending = pending_name = nil
|
|
215
|
+
begin
|
|
216
|
+
loop do
|
|
217
|
+
kind, reply, name, rargs, rkwargs = port.receive
|
|
218
|
+
pending, pending_name = (kind == :async ? nil : reply), name
|
|
219
|
+
begin
|
|
220
|
+
result = servant.__send__(name, *rargs, **rkwargs)
|
|
221
|
+
rescue Exception => e
|
|
222
|
+
if kind == :async
|
|
223
|
+
pending = nil
|
|
224
|
+
__ao_async_exception__(servant, e, name)
|
|
225
|
+
else
|
|
226
|
+
# Cleared only once the reply is out: killed in between, the
|
|
227
|
+
# ensure below would find nothing pending and answer nobody.
|
|
228
|
+
__ao_reply__(reply, :raise, e)
|
|
229
|
+
pending = nil
|
|
230
|
+
end
|
|
231
|
+
next
|
|
232
|
+
end
|
|
233
|
+
if kind == :async
|
|
234
|
+
pending = nil
|
|
235
|
+
next
|
|
236
|
+
end
|
|
237
|
+
result = proxy if result.equal?(servant) # returning self yields the proxy, not a copy
|
|
238
|
+
__ao_reply__(reply, :ok, result)
|
|
239
|
+
pending = nil
|
|
240
|
+
end
|
|
241
|
+
ensure
|
|
242
|
+
# Going down (e.g. the method killed this thread) with a request in
|
|
243
|
+
# flight: its caller waits on the reply port only, so answer it.
|
|
244
|
+
if pending
|
|
245
|
+
__ao_reply__(pending, :raise, Error.new("owner Ractor terminated during #{klass}##{pending_name}"))
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def __ao_async_exception__(servant, exc, name)
|
|
251
|
+
servant.on_async_exception(exc, name)
|
|
252
|
+
rescue Exception => e
|
|
253
|
+
warn "#{servant.class}#on_async_exception raised #{e.class}: #{e.message}"
|
|
254
|
+
end
|
|
255
|
+
private :__ao_async_exception__
|
|
256
|
+
|
|
257
|
+
# Send a result/exception back; values that cannot cross Ractors are
|
|
258
|
+
# converted into an Error so the caller never hangs.
|
|
259
|
+
def __ao_reply__(port, status, payload)
|
|
260
|
+
if status == :raise
|
|
261
|
+
port << [:raise, payload, payload.backtrace]
|
|
262
|
+
else
|
|
263
|
+
port << [:ok, payload]
|
|
264
|
+
end
|
|
265
|
+
rescue Ractor::ClosedError
|
|
266
|
+
nil # caller gave up; nothing to do
|
|
267
|
+
rescue StandardError => e
|
|
268
|
+
msg = if status == :raise
|
|
269
|
+
"#{payload.class}: #{payload.message} (exception could not be transferred: #{e.message})"
|
|
270
|
+
else
|
|
271
|
+
"return value could not be transferred: #{e.message}"
|
|
272
|
+
end
|
|
273
|
+
port << [:raise, Error.new(msg), (payload.backtrace if status == :raise)]
|
|
274
|
+
end
|
|
275
|
+
private :__ao_reply__
|
|
276
|
+
|
|
277
|
+
# Round trip on a reply port taken from a Ractor-local pool; the block
|
|
278
|
+
# sends the request. Waiting on the reply port alone (rather than also
|
|
279
|
+
# watching the owner Ractor) keeps the call cheap; a dying owner answers
|
|
280
|
+
# its in-flight request from __ao_run__, and later sends fail with
|
|
281
|
+
# ClosedError. A port is private to the caller while in use, so
|
|
282
|
+
# concurrent threads never mix replies; one whose wait was interrupted
|
|
283
|
+
# may still get a reply later, so it is not returned to the pool.
|
|
284
|
+
def __ao_sync_call__
|
|
285
|
+
pool = Ractor.store_if_absent(:__ao_reply_ports__) { [] }
|
|
286
|
+
reply = pool.pop || Ractor::Port.new # Array#pop/push are atomic under the GVL
|
|
287
|
+
yield reply
|
|
288
|
+
msg = reply.receive
|
|
289
|
+
pool.push(reply)
|
|
290
|
+
msg
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Backtraces are lost when exceptions cross Ractors; rebuild: owner frames, then caller frames.
|
|
294
|
+
def __ao_restore_exception__(exc, backtrace)
|
|
295
|
+
exc = exc.dup if exc.frozen? # a frozen shareable exception crosses by reference
|
|
296
|
+
exc.set_backtrace(Array(backtrace) + caller(2))
|
|
297
|
+
exc
|
|
298
|
+
rescue FrozenError
|
|
299
|
+
exc # un-dupable: better without the backtrace than masked
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Called on the owner Ractor when an +async+ invocation raises.
|
|
304
|
+
# Override to supervise; the default only reports it.
|
|
305
|
+
def on_async_exception(exception, method_name)
|
|
306
|
+
lines = ["#{self.class}##{method_name} (async) raised #{exception.class}: #{exception.message}"]
|
|
307
|
+
lines.concat(Array(exception.backtrace).map { |l| "\tfrom #{l}" })
|
|
308
|
+
warn lines.join("\n")
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "active_object"
|
|
4
|
+
|
|
5
|
+
class Ractor
|
|
6
|
+
# A Hash kept by a Ractor of its own. Reading it is a question you ask; every
|
|
7
|
+
# change is work you send, and the work runs where the Hash is.
|
|
8
|
+
#
|
|
9
|
+
# h = Ractor::ActorHash.new
|
|
10
|
+
# h.increment(:hits) # send it and carry on
|
|
11
|
+
# h[:hits] # ask
|
|
12
|
+
# h.async_call {|h| (h[:log] ||= []) << line }
|
|
13
|
+
#
|
|
14
|
+
# Because the entries never leave that Ractor except as copies, they do not
|
|
15
|
+
# have to be shareable the way Ractor::LockHash's do: a value can be an Array
|
|
16
|
+
# you go on appending to.
|
|
17
|
+
class ActorHash < ActiveObject
|
|
18
|
+
def initialize(initial = nil)
|
|
19
|
+
@h = initial.nil? ? {} : initial.to_hash.dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Sent and not waited for: the arguments travel as arguments, so unlike a
|
|
23
|
+
# block they can be anything, reassigned locals included.
|
|
24
|
+
async def set(key, value)
|
|
25
|
+
@h[key] = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
async def increment(key, by = 1)
|
|
29
|
+
@h[key] = (@h[key] || 0) + by
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sync def [](key) = @h[key]
|
|
33
|
+
sync def key?(key) = @h.key?(key)
|
|
34
|
+
sync def keys = @h.keys
|
|
35
|
+
sync def to_h = @h.dup
|
|
36
|
+
|
|
37
|
+
# Not published, so they stay off the proxy's API; the proxy reaches them
|
|
38
|
+
# with sync_send, which gets at any method the way __send__ does.
|
|
39
|
+
|
|
40
|
+
# [found, value]; keeps a stored nil apart from a missing key.
|
|
41
|
+
def __lookup__(key) = @h.key?(key) ? [true, @h[key]] : [false, nil]
|
|
42
|
+
|
|
43
|
+
# Runs the block on the owner, handing it the real Hash -- not a copy and
|
|
44
|
+
# not a proxy. It cannot escape: the block runs here, and a return value is
|
|
45
|
+
# copied on its way back, so what the caller gets is never this Hash.
|
|
46
|
+
#
|
|
47
|
+
# The proc arrives as a plain argument, not as a block: blocks cannot cross
|
|
48
|
+
# Ractors, isolated procs can.
|
|
49
|
+
def __call__(prc, args) = prc.call(@h, *args)
|
|
50
|
+
|
|
51
|
+
# The caller-side half. These live on the proxy because that is what
|
|
52
|
+
# ActiveObject hands out.
|
|
53
|
+
class Proxy
|
|
54
|
+
# Which Ractor keeps it is nobody's business out here.
|
|
55
|
+
undef_method :owner, :owner?
|
|
56
|
+
|
|
57
|
+
# Sends the block to the owner and carries on. Returns nil. An exception
|
|
58
|
+
# reaches the owner's #on_async_exception.
|
|
59
|
+
#
|
|
60
|
+
# h.async_call {|h| h[:hits] += 1 }
|
|
61
|
+
# h.async_call(line) {|h, line| h[:log] << line }
|
|
62
|
+
#
|
|
63
|
+
# The block must be isolatable: it may read outer variables that are never
|
|
64
|
+
# reassigned, and anything else has to be passed as an argument.
|
|
65
|
+
def async_call(*args, &block) = async_send(:__call__, isolate(block), args)
|
|
66
|
+
|
|
67
|
+
# Sends the block and waits, returning what it returned, copied back.
|
|
68
|
+
def call(*args, &block) = sync_send(:__call__, isolate(block), args)
|
|
69
|
+
|
|
70
|
+
# Sends the block and returns a Future straight away.
|
|
71
|
+
def future_call(*args, &block) = future_send(:__call__, isolate(block), args)
|
|
72
|
+
|
|
73
|
+
def fetch(key, *default, &block)
|
|
74
|
+
raise ArgumentError, "wrong number of arguments (given #{default.size + 1}, expected 1..2)" if default.size > 1
|
|
75
|
+
|
|
76
|
+
found, value = sync_send(:__lookup__, key)
|
|
77
|
+
return value if found
|
|
78
|
+
return block.call(key) if block # Hash#fetch prefers the block too
|
|
79
|
+
return default.first if default.size == 1
|
|
80
|
+
|
|
81
|
+
raise KeyError.new("key not found: #{key.inspect}", key: key, receiver: self)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def inspect = "#<#{active_object_class} #{to_h.inspect}>"
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# A block that is already shareable is already isolated; making another one
|
|
89
|
+
# allocates a Proc on every call, which on a hot path is most of the cost.
|
|
90
|
+
def isolate(block)
|
|
91
|
+
raise LocalJumpError, "no block given (yield)" unless block
|
|
92
|
+
return block if Ractor.shareable?(block)
|
|
93
|
+
|
|
94
|
+
Ractor.shareable_proc(&block)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sharing/version"
|
|
4
|
+
require_relative "lockvar"
|
|
5
|
+
require_relative "lockhash"
|
|
6
|
+
require_relative "tvar"
|
|
7
|
+
require_relative "active_object"
|
|
8
|
+
require_relative "actor_hash"
|
|
9
|
+
|
|
10
|
+
class Ractor
|
|
11
|
+
# Ways for Ractors to share mutable state. Each one is shareable itself and
|
|
12
|
+
# keeps state that any Ractor may read and change; they differ in how much
|
|
13
|
+
# state moves at once:
|
|
14
|
+
#
|
|
15
|
+
# Ractor::TVar one or more variables, changed together -- start here
|
|
16
|
+
# Ractor::LockVar one variable, and a block that runs exactly once
|
|
17
|
+
# Ractor::LockHash a hash of those, atomic across its own keys
|
|
18
|
+
# Ractor::ActiveObject an object of your own, owned by one Ractor
|
|
19
|
+
# Ractor::ActorHash the same, with the interface already chosen: a hash
|
|
20
|
+
#
|
|
21
|
+
# Require this file for all of them, or one at a time:
|
|
22
|
+
#
|
|
23
|
+
# require "ractor/tvar"
|
|
24
|
+
# require "ractor/lockvar"
|
|
25
|
+
# require "ractor/lockhash"
|
|
26
|
+
# require "ractor/active_object"
|
|
27
|
+
# require "ractor/actor_hash"
|
|
28
|
+
module Sharing
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/ractor/tvar.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ractor-sharing
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Koichi Sasada
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'Ractor::LockVar, Ractor::TVar and Ractor::ActiveObject: shareable objects
|
|
13
|
+
that hold state any Ractor may read and change.'
|
|
14
|
+
email:
|
|
15
|
+
- ko1@atdot.net
|
|
16
|
+
executables: []
|
|
17
|
+
extensions:
|
|
18
|
+
- ext/ractor/lock/extconf.rb
|
|
19
|
+
- ext/ractor/tvar/extconf.rb
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- docs/active_object.md
|
|
25
|
+
- docs/actor_hash.md
|
|
26
|
+
- docs/lockhash.md
|
|
27
|
+
- docs/lockvar.md
|
|
28
|
+
- docs/tvar.md
|
|
29
|
+
- ext/ractor/lock/extconf.rb
|
|
30
|
+
- ext/ractor/lock/lock.c
|
|
31
|
+
- ext/ractor/lock/lock.h
|
|
32
|
+
- ext/ractor/lock/lockhash.c
|
|
33
|
+
- ext/ractor/lock/lockvar.c
|
|
34
|
+
- ext/ractor/tvar/extconf.rb
|
|
35
|
+
- ext/ractor/tvar/tvar.c
|
|
36
|
+
- lib/ractor/active_object.rb
|
|
37
|
+
- lib/ractor/active_object/future.rb
|
|
38
|
+
- lib/ractor/actor_hash.rb
|
|
39
|
+
- lib/ractor/lockhash.rb
|
|
40
|
+
- lib/ractor/lockvar.rb
|
|
41
|
+
- lib/ractor/sharing.rb
|
|
42
|
+
- lib/ractor/sharing/version.rb
|
|
43
|
+
- lib/ractor/tvar.rb
|
|
44
|
+
homepage: https://github.com/ko1/ractor-sharing
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata:
|
|
48
|
+
source_code_uri: https://github.com/ko1/ractor-sharing
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '4.0'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 4.0.6
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Ways for Ractors to share mutable state
|
|
66
|
+
test_files: []
|