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.
