On 06/01/2013 12:08 AM, Vadim wrote:
On Fri, May 31, 2013 at 3:45 PM, Brian Anderson <[email protected]
<mailto:[email protected]>> wrote:
With this problem in general I think the obvious solutions amount
to taking one of two approaches: translate I/O events into pipe
events; translate pipe events into I/O events. Solving the problem
efficiently for either one is rather simpler than solving both.
The example you show is promising model that looks like it could
naively be implemented by buffering I/O into pipes. bblum and I
talked about an implementation that would work well for this
approach, but it has costs. I imagine it working like this.
1) The resolve_xxx function partitions the elements into pipesy
types and I/O types.
3) For each of the I/O types it creates a new pipe, and registers
a uv event. Note that because of I/O-scheduler affinity some of
these may cause the task to migrate between threads.
4) Now we just wait on all the pipes.
What if futures were treated as one-shot pipes with one element queue
capacity, and "real" pipes were used only when there's a need for
buffering? That would help to reduce per-operation costs (also see
notes below about allocation).
oneshot pipes already behave this way but they do incur one allocation
that is shared between both endpoints. I expect futures to be oneshot
pipes with some extra promise semantics on the sender side.
I think we would want to do this too for efficiency reasons. The
above outline has two major costs: the first is the extra pipes
and the second is the buffering of the I/O. For example, the
synchronous read method looks like `read(buf: &mut [u8])` where
the buf is typically allocated on the stack. In the future
scenario presumably it would be more like `read() ->
Future<~[u8]>`, forcing a heap allocation, but maybe `read(buf:
&mut [u8]) -> Future<&mut [u8]>` is workable.
Not necessarily. In fact, .NET's signature of the read method is
something like this: fn read(buf: &mut [u8]) -> ~Future<int>; It
returns just the read bytes count. This is perfectly fine for simple
usage because caller still has a reference to the buffer.
Now, if you want to send it over to another task, this is indeed a
problem, however composition of future comes to the rescue. Each
.NET's future has a method that allows to attach a continuation that
yields a new future of the type of continuation's result:
trait Future<T>
{
fn continue_with<T1>(&self, cont: fn (T) -> T1) -> Future<T1>;
}
let buf = ~[0,..1024];
let f1 = stream.read(&buf);
let f2 : Future<(~[u8],int> = f1.continue_with(|read| return (buf, read));
Now f2 contains all information needed to process received data in
another task.
There is at least one problem with this formulation I think that makes
it unsafe. To start with, for maximum efficiency you want buf to be on
the stack, `[0,..1024]`. If one is ok with using a heap buffer then
there aren't any lifetime issues. So I would want to write:
// A stack buffer
let buf = [0, ..1024];
// A future that tells us when the stack buffer is written
let f = stream.read(&buf);
But because `buf` is on the stack, it must stay valid until `f` is
resolved, which would require a fairly intricate application of
borrowing to guarantee. To make borrowcheck inforce this invariant you
would need the returned future to contain a pointer borrowed from `buf`:
let f: Future<&some_type_that_keeps_the_buf_lifetime_borrowed> =
stream.read(&buf);
This would prevent `buf` from going out of scope and `f` from being
moved out of the `buf` region.
Once you do that though the future is unsendable. To make it sendable we
can possibly implement some fork/join abstraction that lets you borrow
sendable types into subtasks that don't outlive the parent tasks' region
(i'm working on some abstractions for simple fork/join type stuff
currently).
2. Each i/o operation now needs to allocate heap memory for the
future object. This has been known to create GC performance
problems for .NET web apps which process large numbers of small
requests. If these can live on the stack, though, maybe this
wouldn't be a problem for Rust.
Haha, yep that's a concern.
I know that Rust currently doesn't currently support this, but what if
futures could use a custom allocator? Then it could work like this:
1. Futures use a custom free-list allocator for performance.
2. The I/O request allocates new future object, registers uv event,
then returns unique pointer to the future to its' caller. However I/O
manager retains internal reference to the future, so that it can be
resolved once I/O completes.
3. The future object also has a flag indicating that there's an
outstanding I/O, so if caller drops the reference to it, it won't be
returned to the free list until I/O completes.
4. When I/O is complete, the future get resolved and all attached
continuations are run.
A lot of this behaviour around managing the future's allocation exists
in pipes and futures will almost certainly be based on oneshot pipes. I
do expect that pipes will ultimately want some control over their
allocation so they can reuse buffers.
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev