Project

General

Profile

Feature #17298

Ractor's basket communication APIs

Added by ko1 (Koichi Sasada) 26 days ago. Updated 13 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:100673]

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.

#1

Updated by ko1 (Koichi Sasada) 26 days ago

  • Description updated (diff)
#2

Updated by ko1 (Koichi Sasada) 26 days ago

  • Description updated (diff)
#3

Updated by ko1 (Koichi Sasada) 26 days ago

  • Description updated (diff)

Updated by Eregon (Benoit Daloze) 26 days 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) 26 days 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) 26 days 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) 25 days 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 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.

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.
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.

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 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.

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) 25 days 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.

#9

Updated by sawa (Tsuyoshi Sawada) 25 days ago

  • Description updated (diff)

Updated by ko1 (Koichi Sasada) 24 days 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) 23 days 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) 23 days 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) 23 days 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) 23 days 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) 22 days 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) 22 days ago

Eregon (Benoit Daloze) wrote in #note-15:

Ractor#receive_and_sender (aka recvfrom but with a proper name) and Ractor.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) 22 days ago

ko1 (Koichi Sasada) wrote in #note-16:

Eregon (Benoit Daloze) wrote in #note-15:

Ractor#receive_and_sender (aka recvfrom but with a proper name) and Ractor.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.

#18

Updated by Dan0042 (Daniel DeLorme) 13 days ago

  • Description updated (diff)

Updated by Dan0042 (Daniel DeLorme) 13 days 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.

Also available in: Atom PDF