Feature #17298
openRactor's basket communication APIs
Added by ko1 (Koichi Sasada) about 4 years ago. Updated about 4 years ago.
Description
This ticket proposes send_basket
/receive_basket
, yield_basket
/take_basket
APIs to make effective and flexible bridge ractors.
Background¶
When we want to send an object as a message, we usually need to copy it. Copying is achieved according to marshal protocol, and the receiver loads it immediately.
If we want to make a bridge ractor that receives a message and sends it to another ractor, immediate loading is not effective.
bridge = Ractor.new do
Ractor.yield Ractor.receive
end
consumer = Ractor.new bridge do |from|
obj = from.take
do_task(obj)
end
msg = [1, 2, 3]
bridge.send msg
In this case, the array ([1, 2, 3]
) is
- (1) dumped at the first
bridge.send msg
- (2) loaded at
Ractor.receive
- (3) dumped again at
Ractor.yield
- (4) loaded at
from.take
Essentially, we only need one dump/load pair, but now it needs two pairs.
Mixing "moving" status is more complex. Now there is no way to pass the "moving" status to bridge ractors, so we cannot make a moving bridge.
Proposal¶
To make more effective and flexible bridge ractors, we propose new basket APIs
Ractor.receive_basket
Ractor#send_basket
Ractor#take_basket
Ractor.yield_basket
They receive a message, retains the dumped state, and sends it without dumping again. We can rewrite the above example with these APIs.
bridge = Ractor.new do
Ractor.yield_basket Ractor.receive_basket
end
consumer = Ractor.new bridge do |from|
obj = from.take
do_task(obj)
end
msg = [1, 2, 3]
bridge.send msg
In this case,
- (1) dumped at the first
bridge.send msg
- (2) loaded at
from.take
we only need one dump/load pair.
Implementation¶
https://github.com/ruby/ruby/pull/3725
Evaluation¶
The following program makes four types of bridges and passes an array as a message through them.
USE_BASKET = false
receive2yield = Ractor.new do
loop do
if USE_BASKET
Ractor.yield_basket Ractor.receive_basket
else
Ractor.yield Ractor.receive
end
end
end
receive2send = Ractor.new receive2yield do |r|
loop do
if USE_BASKET
r.send_basket Ractor.receive_basket
else
r.send Ractor.receive
end
end
end
take2yield = Ractor.new receive2yield do |from|
loop do
if USE_BASKET
Ractor.yield_basket from.take_basket
else
Ractor.yield from.take
end
end
end
take2send = Ractor.new take2yield, Ractor.current do |from, to|
loop do
if USE_BASKET
to.send_basket from.take_basket
else
to.send from.take
end
end
end
AN = 1_000
LN = 10_000
ary = Array.new(AN) # 1000
LN.times{
receive2send << ary
Ractor.receive
}
# This program passes the message as:
# main ->
# receive2send ->
# receive2yield ->
# take2yield ->
# take2send ->
# main
The result is:
w/ basket API 0m2.056s
w/o basket API 0m5.974s
on my machine (=~ x3 faster).
(BTW, if we have a TVar, we can change the value USE_BASKET
dynamically)
Discussion¶
Naming¶
Of course, naming is an issue. Now, I named it "_basket" because the source code uses this terminology. There are other candidates:
- container metaphor
- package
- parcel
- box
- envelope
- packet (maybe bad idea because of confusion of networking)
- bundle (maybe bad idea because of confusion of bin/bundle)
- "don't touch the content" metaphor
- raw
- sealed
- unopened
I like "basket" because I like picnic.
Feature¶
Now, basket is represented by "Ractor::Basket" and there are no methods. We can add the following feature:
-
Ractor::Basket#sender
returns the sending ractor. -
Ractor::Basket#sender = a_ractor
changes the sending ractor. -
Ractor::Basket#value
returns the content.
There was another proposal Ractor.recvfrom
, but we only need these APIs.
Updated by Eregon (Benoit Daloze) about 4 years ago
For the first example, isn't move: true
much simpler?
bridge = Ractor.new do
Ractor.yield Ractor.receive, move: true
end
consumer = Ractor.new bridge do |from|
obj = from.take
do_task(obj)
end
msg = [1, 2, 3]
bridge.send msg
Having move: true
on Ractor.yield
means we need to wait to know which Ractor to move the object to, but that seems fine and transparent to the user.
The bridge Ractor can ensure it will no longer refer to the message, so it's perfectly safe to move there.
Similar for the other 3 examples in the benchmark.
Adding 4 new methods for this seem heavy to me. Also, exposing the serialized representation seems bad.
For instance, there might be an efficient deep_copy/dup/clone in the future, and then there is no reason to use Marshal anymore, and no serialized representation.
Explicitly deeply freezing the message (e.g., with Ractor.make_shareable, or some kwarg to freeze on send/yield) seems a good way too, and easier to reason about.
Sending a copy of a mutable object seems useless to me, because mutations will not affect the sender, it only mutates a copy.
So either freezing or moving seems more useful, and both avoid copying.
Updated by Eregon (Benoit Daloze) about 4 years ago
From the benchmark above, USE_BASKET=false takes for me 5.636s
.
Adding Ractor.make_shareable(ary)
, it takes 0.192s
.
So that's ~10x faster than with w/ basket API
on this benchmark, and does not require new APIs and concepts.
So I don't see the need for this API, better to freeze or to move, and both are more efficient.
Copying is confusing anyway if mutated.
Maybe all messages should be made Ractor.make_shareable
by Ractor#send
/Ractor.yield
, unless move: true
is used?
Then there would be no confusion about mutations, and there would be no hidden cost to sending a message (copying a big data structure can take a long time as we see here).
Moving still has a cost proportional to the #objects in the object graph, which seems unavoidable, but at least it does not need to copy e.g. the array storage.
Do other languages have something similar to the basket API?
Updated by Eregon (Benoit Daloze) about 4 years ago
In the example above, using , move: true
for Ractor.yield and Ractor#send, instead of the _basket
calls seems to give the same or better performance.
(code at https://gist.github.com/eregon/092ea76534b46e227d9cbf5fd107de66)
It runs in 1.578s for me.
And it requires less modifications than the _basket
APIs which need both sides to know about it, not just the sending side.
Updated by ko1 (Koichi Sasada) about 4 years ago
Eregon (Benoit Daloze) wrote in #note-4:
For the first example, isn't
move: true
much simpler?
There are two problems:
- Now, many types are not support on
move: true
. I'll investigate more, but not sure we can support for all data types. - To move the object, we need traverse the object each time and make shallow copy for it. For example, we need to traverse a nesting array.
Having
move: true
onRactor.yield
means we need to wait to know which Ractor to move the object to, but that seems fine and transparent to the user.
The bridge Ractor can ensure it will no longer refer to the message, so it's perfectly safe to move there.
If we analyze cross-method escaping, it can be. But now there is no support yet.
bridge = Ractor.new do
msg = Ractor.receive
Ractor.yield msg, move: true
end
Adding 4 new methods for this seem heavy to me. Also, exposing the serialized representation seems bad.
"serialized representation" is not exposed.
It only says the bridge ractor doesn't touch the message.
Explicitly deeply freezing the message (e.g., with Ractor.make_shareable, or some kwarg to freeze on send/yield) seems a good way too, and easier to reason about.
Sending a copy of a mutable object seems useless to me, because mutations will not affect the sender, it only mutates a copy.
So either freezing or moving seems more useful, and both avoid copying.
I agree to add an option calling make_shareable
to send
and yield
.
It is already filed in my working memo.
However, it doesn't mean we can avoid copying.
I believe sending copy is the most used option because there is no difficulties.
Sender does not need to care about accessing to the sent object after sending.
Eregon (Benoit Daloze) wrote in #note-5:
From the benchmark above, USE_BASKET=false takes for me
5.636s
.
AddingRactor.make_shareable(ary)
, it takes0.192s
.
So that's ~10x faster than withw/ basket API
on this benchmark, and does not require new APIs and concepts.
Of course, it is faster.
So I don't see the need for this API, better to freeze or to move, and both are more efficient.
Copying is confusing anyway if mutated.
As I wrote, I believe copying should be the first option.
Maybe all messages should be made
Ractor.make_shareable
byRactor#send
/Ractor.yield
, unlessmove: true
is used?
Then there would be no confusion about mutations, and there would be no hidden cost to sending a message (copying a big data structure can take a long time as we see here).
Moving still has a cost proportional to the #objects in the object graph, which seems unavoidable, but at least it does not need to copy e.g. the array storage.
It can be one option, but I think it is introducing huge side-effect.
Do other languages have something similar to the basket API?
I don't have an idea. I don't have any
- copying is used for IPC
- immutable data is used for traditional actor models
- only reference (pointer) is used for shared-everything model
I don't know our mixing model.
Eregon (Benoit Daloze) wrote in #note-6:
In the example above, using
, move: true
for Ractor.yield and Ractor#send, instead of the_basket
calls seems to give the same or better performance.
(code at https://gist.github.com/eregon/092ea76534b46e227d9cbf5fd107de66)
It runs in 1.578s for me.
AN = 1_000
LN = 100
ary = Array.new(AN){Array.new(AN)}
Changing the parameter, it takes:
use basket: 0m6.047s
use move : 0m8.018s
There is a bit difference.
And it requires less modifications than the
_basket
APIs which need both sides to know about it, not just the sending side.
In general, proposed APIs should be hidden in framework, I think.
Updated by ko1 (Koichi Sasada) about 4 years ago
BTW, for recvfrom
feature, we need more 2 APIs.
This API can cover this feature (and it is more extendable), so I don't think "4" APIs are heavy.
Updated by ko1 (Koichi Sasada) about 4 years ago
I rewrote copying logic without marshal protocol (https://github.com/ruby/ruby/pull/3728).
Additional benchmark:
Warning[:experimental] = false
if ENV['MODE'] == 'share'
MODE = :copy
SHAREABLE = true
else
MODE = ENV['MODE'].to_sym
SHAREABLE = false
end
receive2yield = Ractor.new do
loop do
case MODE
when :basket
Ractor.yield_basket Ractor.receive_basket
when :move
Ractor.yield Ractor.receive, move: true
when :copy
Ractor.yield Ractor.receive
else
raise
end
end
end
to = receive2yield
receive2send = Ractor.new to do |to|
loop do
case MODE
when :basket
obj = Ractor.receive_basket
to.send_basket obj
when :move
to.send Ractor.receive, move: true
when :copy
to.send Ractor.receive
end
end
end
take2yield = Ractor.new receive2yield do |from|
loop do
case MODE
when :basket
Ractor.yield_basket from.take_basket
when :move
Ractor.yield from.take, move: true
when :copy
Ractor.yield from.take
end
end
end
take2send = Ractor.new take2yield, Ractor.current do |from, to|
loop do
case MODE
when :basket
to.send_basket from.take_basket
when :move
to.send from.take, move: true
when :copy
to.send from.take
end
end
end
AN = ENV['AN'].to_i
LN = 10_000
obj = Array.new(AN/10){Array.new{AN}}
Ractor.make_shareable(obj) if SHAREABLE
LN.times{
receive2send.send obj
Ractor.receive
}
__END__
obj = Array.new(AN){}
user system total real
share/0 0.000235 0.000000 2.084210 ( 1.244361)
basket/0 0.000186 0.000000 2.157927 ( 1.309404)
move/0 0.000193 0.000000 2.117845 ( 1.294796)
copy/0 0.000164 0.000000 2.324445 ( 1.431408)
share/30 0.000113 0.000000 2.086155 ( 1.235768)
basket/30 0.000158 0.000000 2.197975 ( 1.335114)
move/30 0.000211 0.000000 2.190369 ( 1.352113)
copy/30 0.000171 0.000000 2.389695 ( 1.490143)
share/60 0.000206 0.000000 1.997523 ( 1.191689)
basket/60 0.000160 0.000000 2.237804 ( 1.356830)
move/60 0.000145 0.000000 2.268976 ( 1.397970)
copy/60 0.000185 0.000000 2.538057 ( 1.580106)
share/90 0.000168 0.000000 2.069107 ( 1.224712)
basket/90 0.000148 0.000000 2.186683 ( 1.327779)
move/90 0.000158 0.000000 2.154334 ( 1.330732)
copy/90 0.000139 0.000000 2.504655 ( 1.561725)
share/120 0.000189 0.000000 2.120684 ( 1.255453)
basket/120 0.000202 0.000000 2.270650 ( 1.375921)
move/120 0.000151 0.000000 2.416082 ( 1.491159)
copy/120 0.000156 0.000000 2.436406 ( 1.554357)
share/150 0.000216 0.000000 2.103629 ( 1.248967)
basket/150 0.000219 0.000000 2.323634 ( 1.408301)
move/150 0.000187 0.000000 2.123332 ( 1.325794)
copy/150 0.000000 0.000205 2.519390 ( 1.592612)
share/180 0.000000 0.000149 1.916865 ( 1.143092)
basket/180 0.000000 0.000229 2.339701 ( 1.416612)
move/180 0.000000 0.000188 2.384908 ( 1.497792)
copy/180 0.000000 0.000163 2.503268 ( 1.593776)
share/210 0.000000 0.000128 1.987748 ( 1.176819)
basket/210 0.000000 0.000167 2.228943 ( 1.353803)
move/210 0.000000 0.000177 2.431323 ( 1.519974)
copy/210 0.000000 0.000190 2.592614 ( 1.652112)
share/240 0.000000 0.000206 2.063813 ( 1.221907)
basket/240 0.000000 0.000222 2.266633 ( 1.383860)
move/240 0.000000 0.000140 2.291410 ( 1.442910)
copy/240 0.000000 0.000129 2.360580 ( 1.519305)
share/270 0.000000 0.000200 2.187821 ( 1.292601)
basket/270 0.000000 0.000141 2.186951 ( 1.335748)
move/270 0.000000 0.000175 2.331970 ( 1.462081)
copy/270 0.000000 0.000201 2.544173 ( 1.631321)
share/300 0.000000 0.000220 2.103491 ( 1.259986)
basket/300 0.000000 0.000208 2.470096 ( 1.533962)
move/300 0.000000 0.000177 2.477407 ( 1.575883)
copy/300 0.000000 0.000161 2.577555 ( 1.664693)
obj = Array.new(AN/10){Array.new{AN}}
user system total real
share/0 0.000178 0.000000 1.999250 ( 1.192009)
basket/0 0.000186 0.000000 2.113732 ( 1.276346)
move/0 0.000111 0.000000 2.079669 ( 1.272024)
copy/0 0.000261 0.000000 2.301534 ( 1.419984)
share/30 0.000141 0.000000 2.133833 ( 1.264703)
basket/30 0.000157 0.000000 2.314221 ( 1.400378)
move/30 0.000152 0.000000 2.349555 ( 1.461622)
copy/30 0.000180 0.000000 2.255883 ( 1.421459)
share/60 0.000217 0.000000 2.095241 ( 1.243948)
basket/60 0.000206 0.000000 2.239443 ( 1.372667)
move/60 0.000129 0.000000 2.477502 ( 1.550481)
copy/60 0.000298 0.000000 2.510642 ( 1.610342)
share/90 0.000159 0.000000 2.241600 ( 1.323923)
basket/90 0.000293 0.000000 2.184617 ( 1.339768)
move/90 0.000239 0.000000 2.524579 ( 1.590508)
copy/90 0.000157 0.000000 2.610485 ( 1.696873)
share/120 0.000130 0.000000 2.164260 ( 1.283117)
basket/120 0.000190 0.000000 2.250227 ( 1.383655)
move/120 0.000239 0.000000 2.299717 ( 1.459356)
copy/120 0.000220 0.000000 2.624845 ( 1.720889)
share/150 0.000175 0.000000 2.024881 ( 1.203234)
basket/150 0.000214 0.000000 2.213022 ( 1.367973)
move/150 0.000230 0.000000 2.351162 ( 1.508806)
copy/150 0.000261 0.000000 2.600007 ( 1.729760)
share/180 0.000128 0.000000 1.981322 ( 1.176595)
basket/180 0.000218 0.000000 2.202996 ( 1.368278)
move/180 0.000190 0.000000 2.684911 ( 1.733626)
copy/180 0.000176 0.000000 2.877426 ( 1.940715)
share/210 0.000162 0.000000 2.096774 ( 1.243471)
basket/210 0.000119 0.000000 2.227222 ( 1.387271)
move/210 0.000277 0.000000 2.654121 ( 1.725943)
copy/210 0.000198 0.000000 2.823281 ( 1.916791)
share/240 0.000166 0.000000 2.209040 ( 1.309859)
basket/240 0.000169 0.000000 2.367142 ( 1.475902)
move/240 0.000144 0.000000 2.600334 ( 1.702996)
copy/240 0.000164 0.000000 2.859249 ( 1.956904)
share/270 0.000212 0.000000 2.100495 ( 1.244568)
basket/270 0.000234 0.000000 2.346845 ( 1.464750)
move/270 0.000165 0.000000 2.695428 ( 1.770649)
copy/270 0.000184 0.000000 2.934659 ( 2.020429)
share/300 0.000169 0.000000 2.145326 ( 1.269930)
basket/300 0.000160 0.000000 2.286473 ( 1.426499)
move/300 0.000213 0.000000 2.706252 ( 1.789096)
copy/300 0.000270 0.000000 3.063131 ( 2.136394)
obj = "*" * (100 * AN)
user system total real
share/0 0.000000 0.000225 2.113924 ( 1.256272)
basket/0 0.000000 0.000234 2.103315 ( 1.275444)
move/0 0.000000 0.000170 2.270854 ( 1.393821)
copy/0 0.000000 0.000160 2.311942 ( 1.424493)
share/30 0.000000 0.000209 2.190677 ( 1.290621)
basket/30 0.000000 0.000126 2.201340 ( 1.338486)
move/30 0.000000 0.000133 2.362480 ( 1.438814)
copy/30 0.000000 0.000155 2.564007 ( 1.623138)
share/60 0.000000 0.000230 2.045274 ( 1.215421)
basket/60 0.000000 0.000175 2.240818 ( 1.358289)
move/60 0.000000 0.000230 2.241105 ( 1.366148)
copy/60 0.000147 0.000000 2.582220 ( 1.621733)
share/90 0.000181 0.000000 2.045982 ( 1.219508)
basket/90 0.000152 0.000000 2.259071 ( 1.368956)
move/90 0.000229 0.000000 2.215262 ( 1.353770)
copy/90 0.000193 0.000000 2.311903 ( 1.465815)
share/120 0.000189 0.000000 2.220438 ( 1.309290)
basket/120 0.000182 0.000000 2.337383 ( 1.415203)
move/120 0.000216 0.000000 2.185714 ( 1.342087)
copy/120 0.000174 0.000000 2.547647 ( 1.612203)
share/150 0.000175 0.000000 2.111274 ( 1.247957)
basket/150 0.000199 0.000000 2.352135 ( 1.436637)
move/150 0.000232 0.000000 2.341878 ( 1.440591)
copy/150 0.000119 0.000000 2.567323 ( 1.629583)
share/180 0.000176 0.000000 2.206774 ( 1.303557)
basket/180 0.000153 0.000000 2.123505 ( 1.295564)
move/180 0.000272 0.000000 2.295597 ( 1.406772)
copy/180 0.000201 0.000000 2.537667 ( 1.620875)
share/210 0.000236 0.000000 2.159986 ( 1.278420)
basket/210 0.000231 0.000000 2.357100 ( 1.449504)
move/210 0.000187 0.000000 2.434216 ( 1.514514)
copy/210 0.000273 0.000000 2.581147 ( 1.654714)
share/240 0.000220 0.000000 2.187199 ( 1.290690)
basket/240 0.000134 0.000000 2.125558 ( 1.308496)
move/240 0.000205 0.000000 2.289436 ( 1.411198)
copy/240 0.000158 0.000000 2.561948 ( 1.649240)
share/270 0.000184 0.000000 1.949823 ( 1.151623)
basket/270 0.000181 0.000000 2.305483 ( 1.408048)
move/270 0.000202 0.000000 2.097124 ( 1.294308)
copy/270 0.000139 0.000000 2.613040 ( 1.683508)
share/300 0.000264 0.000000 2.147143 ( 1.270971)
basket/300 0.000165 0.000000 2.260574 ( 1.377311)
move/300 0.000224 0.000000 2.186270 ( 1.345459)
copy/300 0.000280 0.000000 2.640306 ( 1.709781)
- Most of case, tuple or something small objects are used to communicate (
[command, param1, param2]
for example) and it is enough fast (on current inefficient implementation).
obj = Array.new(AN){}
user system total real
share/0 0.000235 0.000000 2.084210 ( 1.244361)
basket/0 0.000186 0.000000 2.157927 ( 1.309404)
move/0 0.000193 0.000000 2.117845 ( 1.294796)
copy/0 0.000164 0.000000 2.324445 ( 1.431408)
share/30 0.000113 0.000000 2.086155 ( 1.235768)
basket/30 0.000158 0.000000 2.197975 ( 1.335114)
move/30 0.000211 0.000000 2.190369 ( 1.352113)
copy/30 0.000171 0.000000 2.389695 ( 1.490143)
obj = Array.new(AN/10){Array.new{AN}}
user system total real
share/0 0.000178 0.000000 1.999250 ( 1.192009)
basket/0 0.000186 0.000000 2.113732 ( 1.276346)
move/0 0.000111 0.000000 2.079669 ( 1.272024)
copy/0 0.000261 0.000000 2.301534 ( 1.419984)
share/30 0.000141 0.000000 2.133833 ( 1.264703)
basket/30 0.000157 0.000000 2.314221 ( 1.400378)
move/30 0.000152 0.000000 2.349555 ( 1.461622)
copy/30 0.000180 0.000000 2.255883 ( 1.421459)
obj = "*" * (100 * AN)
user system total real
share/0 0.000000 0.000225 2.113924 ( 1.256272)
basket/0 0.000000 0.000234 2.103315 ( 1.275444)
move/0 0.000000 0.000170 2.270854 ( 1.393821)
copy/0 0.000000 0.000160 2.311942 ( 1.424493)
share/30 0.000000 0.000209 2.190677 ( 1.290621)
basket/30 0.000000 0.000126 2.201340 ( 1.338486)
move/30 0.000000 0.000133 2.362480 ( 1.438814)
copy/30 0.000000 0.000155 2.564007 ( 1.623138)
- On nested data structure, "basket" seems fine.
Updated by marcandre (Marc-Andre Lafortune) about 4 years ago
The use-case seems unusual:
- Need to send very big object to a Ractor
- That object can not be deep-frozen for some reason, or moved so it deep-copied
- The receiving Ractor needs to send it to another Ractor
Even given this "extreme" case, the numbers don't show a very big difference, so I would say it is probably not a good idea to introduce now with the limited knowledge we have now. We'll have a better idea of how Ractor is used and what the bottlenecks are after it gains traction.
Updated by Eregon (Benoit Daloze) about 4 years ago
marcandre (Marc-Andre Lafortune) wrote in #note-11:
The use-case seems unusual:
- Need to send very big object to a Ractor
- That object can not be deep-frozen for some reason, or moved so it deep-copied
That sounds like the perfect anti-pattern to me, so agreed this new APIs seem overkill for now and not worth it.
If it's a big object:
- If it can be frozen, freeze it.
- If it must be mutable, then it should have its own Ractor that "encapsulates" it.
- Copying doesn't seem to make sense if it's mutated (because it will only mutate one of the copies). But anyway one can still use the existing Ractor#send/yield APIs to copy it.
Updated by Eregon (Benoit Daloze) about 4 years ago
ko1 (Koichi Sasada) wrote in #note-10:
I rewrote copying logic without marshal protocol (https://github.com/ruby/ruby/pull/3728).
That is great!
I remember doing something quite similar in https://eregon.me/thesis/mozart2slides.pdf (slides 28-35)
Updated by ko1 (Koichi Sasada) about 4 years ago
Beside the performance, I want to use it for the features:
Ractor::Basket#sender returns the sending ractor.
Ractor::Basket#sender = a_ractor changes the sending ractor.
Ractor::Basket#value returns the content.
Updated by Eregon (Benoit Daloze) about 4 years ago
ko1 (Koichi Sasada) wrote in #note-14:
Beside the performance, I want to use it for the features:
Ractor::Basket#sender returns the sending ractor.
Ractor#receive_and_sender
(aka recvfrom
but with a proper name) and Ractor.yield_and_sender
would be enough for that, right?
Of course, the sender Ractor can send Ractor.current
, so this can already be emulated.
Ractor::Basket#sender = a_ractor changes the sending ractor.
Any concrete use-case?
It sounds bad to me to use core types to maintain custom state.
Also if this affects the first basket instance, it would expose mutable state and data races.
The Basket instance should probably be deeply frozen anyway (if introduced).
Updated by ko1 (Koichi Sasada) about 4 years ago
Eregon (Benoit Daloze) wrote in #note-15:
Ractor#receive_and_sender
(akarecvfrom
but with a proper name) andRactor.yield_and_sender
would be enough for that, right?
Yes, it is enough. However, the name is too long and ambiguous (maybe receive_value_and_its_sender
).
I like basket API for this purpose.
Also we can extend the information, for example sending source location (for debugging purpose).
Ractor::Basket#sender = a_ractor changes the sending ractor.
Any concrete use-case?
Not sure now.
It sounds bad to me to use core types to maintain custom state.
Also if this affects the first basket instance, it would expose mutable state and data races.
The Basket instance should probably be deeply frozen anyway (if introduced).
No, Basket is copied every time on current implementation (one basket object is created on receiving timing).
But I agree we can make it frozen and shareable.
Updated by Eregon (Benoit Daloze) about 4 years ago
ko1 (Koichi Sasada) wrote in #note-16:
Eregon (Benoit Daloze) wrote in #note-15:
Ractor#receive_and_sender
(akarecvfrom
but with a proper name) andRactor.yield_and_sender
would be enough for that, right?
Actually it would be Ractor#receive_with_sender
and Ractor#take_with_sender
(or some variation, but for receive+take), my mistake
(I was thinking about Fiber.yield which also waits for a message back).
We can only know the sender when "receiving a message".
Knowing the sender when sending a message is useless, it's always Ractor.current.
I like basket API for this purpose.
Also we can extend the information, for example sending source location (for debugging purpose).
I see, Ractor#receive_basket
and Ractor#take_basket
might be nicer to get more information when receiving a message.
send_basket
and yield_basket
seem unnecessary, if send/yield
already recognize it's a basket and extract the value out of it.
Or, if send/yield
explicitly disallow sending a basket and require ractor.yield basket.value
(since it's meaningless to pass the sender's sender there).
That would not allow the optimization, but the API feels cleaner to me that way.
A basket is then just a (value, sender) tuple, and not something that implicitly holds a serialized version of the object.
Also serializing might not have exactly the same semantics as deep copying, so it seems a risk of inconsistency, for very small performance gains.
Updated by Dan0042 (Daniel DeLorme) about 4 years ago
- Description updated (diff)
Updated by Dan0042 (Daniel DeLorme) about 4 years ago
A comment about naming... my first impression was that a "basket" is a container with multiple objects to transmit between Ractors. Something like
basket = Ractor::Basket.new
basket.to_move << a
basket.to_copy << b
basket.to_share << c
Ractor.new(basket) do |basket|
...
end
I found it counterintuitive to understand this was not the proposal. Since it only contains a single value, I think "envelope" is a better metaphor. An envelope usually contains a single letter, whereas a basket usually contains several (picnic) items.
But from an OOP perspective I still don't understand why all those "basket" methods. Since there's already a Ractor::Basket object involved, why not send that via the existing methods? So rather than Ractor.yield_basket Ractor.receive_basket
it feels more OO to have Ractor.yield Basket.new(Ractor.receive)
. Actually in the case of the bridge ractor it would just be Ractor.yield Ractor.receive
, and if the object being passed is a basket then it's efficient, otherwise it's not.