Issue #17298 has been updated by Eregon (Benoit Daloze).


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.

----------------------------------------
Feature #17298: Ractor's basket communication APIs
https://bugs.ruby-lang.org/issues/17298#change-88357

* Author: ko1 (Koichi Sasada)
* Status: Open
* Priority: Normal
----------------------------------------
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, 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.

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

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

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




-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>