Feature #17298
Updated by ko1 (Koichi Sasada) about 4 years ago
This ticket proposes send_basket/send_receive, yield_basket/take_basket APIs to make effective and flexible bridge ractors.
## Background
When we want to send an object as a message, usually we need to copy it.
Copying is achieved by marshal protocol, and receiver load it immediately.
If we want to make a bridge ractor which receive a message and send it to another ractor, the immediate loading is not effective.
Receiver load
```ruby
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) laoded at `from.take`
Essentially we only need one dump/load pair, but now it needs 2 pairs.
Mixing "moving" is more complex.
Now there is no way to pass the "moving" status to the bridge ractors, we can not 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, but remaining dumped state and send it without dumping again.
We can rewrite the above example with these APIs.
```ruby
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) laoded at `from.take`
we only need one dump/load pair.
## Implementation
https://github.com/ruby/ruby/pull/3725
## Evaluation
The following program makes 4 type of bridges and pass an array as a message through them.
```ruby
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 "_basket" because source code using 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 is no methods.
We can add the following feature:
* `Ractor::Basket#sender` return the sending ractor.
* `Ractor::Basket#sender = a_ractor` change the sending ractor.
* `Ractor::Basket#value` returns the content.
There was another proposal `Ractor.recvfrom`, but we only need these APIs.