On Wed, Jul 19, 2017 at 10:26 PM, Ross Light <[email protected]> wrote:

> So in this old thread
> <https://groups.google.com/d/topic/capnproto/4SLfibQPWFE/discussion>,
> it's stated that the "call is received" event requires calling into
> application code.  From an implementation standpoint, this is declaring
> that receiving a call in the RPC system is a critical section that involves
> crossing over into application code boundary, which may try to acquire the
> same mutex (by making a call on the connection).  While you can postpone
> this problem by adding queueing, I'm already a little nervous about how
> much queueing is required by the protocol.
>
> I'd like to suggest that the E model be considered: each capability is a
> separate single queue.  Instead of "call A is received happens before call
> B is received", "call A returns happens before call B starts".  The reason
> this simplifies implementation is that because it prescribes what ought to
> happen in the critical section (enqueue or throw an overload exception),
> then no application needs to be invoked in the critical section.  This
> might not be a problem for the C++ implementation right now, but once
> fibers are involved, I think it would become one.
>

If I understand what you're proposing correctly, then another way of saying
it is: "A call must return a result before the next call on the same object
can begin."

(To be clear, this certainly isn't "the E model". Strictly speaking, in E,
calls don't "return" anything, but they do eventually resolve a promise,
and there's no requirement that that resolution happens before the next
call can proceed.)

I don't think this proposal would work. You're saying that if a method call
foo() wants to allow other methods to be called before foo() produces a
result, then foo() must produce a result via a callback rather than via
return. foo() would need to take, as one of its parameters, a capability
which it calls with the eventual results.

This would, of course, lead to all the usual "callback hell" problems we
see in async I/O. Over time, we've reached a consensus that the right way
to solve "callback hell" is to use promises. Promises allow us to express
the eventual results of an async function as a return value, which is much
more natural than using callbacks. Also, it makes it much clearer how
errors are to be propagated, and makes it harder to accidentally leak
errors.

So the next logical step would be to introduce a notion of promises into
Cap'n Proto interface specs. Let methods return promises instead of raw
values, and then they are free to interleave as necessary.

But then what would happen? Probably, everyone would declare all of their
methods to return promises, to give themselves flexibility to change their
implementation in the future if needed. In fact, there'd be even more
temptation to do this then there is in C++ and Javascript today, because
the client of a Cap'n Proto interface already has to treat the result as a
promise for latency and pipelining reasons. So, making a method return a
promise would create no new inconvenience on the client side (because
clients already have to deal with promises either way), and it would create
no inconvenience on the server side (because you can return an
immediately-resolved promise basically just as easily as returning a
value). So, everyone would declare every method to return a promise.

The next step, then, would be to say: OK, since everyone is declaring all
returns to be promises anyway, we're just going to say that it's implied.
All methods actually return promises. You don't need to declare it.

And then... we'd be back to *exactly* where we are today.

Today, the right way to think about Cap'n Proto methods is to say that all
methods return promises.


> I believe that the same properties can be obtained by pushing this into
> interface definitions: if an interface really wants to declare that
> operations can happen in parallel, then there can be a root capability that
> creates a capability for each operation.  Then the RPC subsystem can know
> much more about how much work is being scheduled.
>
> I realize this would be a big change, but I can't see a good way to avoid
> this problem in any implementation of the RPC protocol that tries to use a
> connection concurrently.  Effectively, this forces all implementations to
> be single-threaded AFAICT.  Let me know what you think.
>

Cap'n Proto technically only requires that each *object* is
single-threaded. It's based on the actor model, which is actually similar
to the CSP model on which Go's concurrency is based. In fact, maybe the
problem is that we're trying to map Cap'n Proto to the wrong idioms in Go.

Imagine this design: Instead of mapping capnp methods to Go functions, we
map them to messages on a Go channel. Each object reads from a channel.
Each message on the channel initiates a call, and specifies a response
channel to which the call results should be sent when they are ready.

This design would achieve E-Order while allowing overlapping calls and
being idiomatic with Go's concurrency model.

Thoughts?

=====================

On another note -- getting away from specific languages or models -- I
don't think it's practical, in most use cases, to try to utilize multiple
processor cores to service a single connection. (Note I'm avoiding the word
"threads" here since languages like Go blur the meaning of "threads"
between processor cores vs. coroutines. I'm talking about cores, not about
coroutines.)

The problem is, the connection is implicitly a synchronization point. You
can't have multiple cores literally reading from the same connection at the
same time. So you necessarily have to have one core (at a time) servicing
the connection and then passing data off to other cores. But, the cost of
synchronization and data transfer between cores is likely to outweigh the
benefit in most use cases. You'd only get a benefit if one client is making
multiple CPU-heavy calls concurrently, which isn't all that common. In all
other cases you'd lose performance.

Instead, I think a better model is to balance whole connections across
cores. A client can make multiple connections if they want to utilize
multiple cores. When three-party handoff is implemented, the server will
even be able to instruct the client to open additional connections,
transparently to the app. This way the OS kernel actually knows which
thread any particular message is destined for, and directly schedules that
thread when a message arrives. No extra context switches!

This approach works (e.g. using Cap'n Proto C++) today.

-Kenton

-- 
You received this message because you are subscribed to the Google Groups 
"Cap'n Proto" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
Visit this group at https://groups.google.com/group/capnproto.

Reply via email to