I have been thinking about the costs and benefits we get from tasks. I
had discussed some of them with Graydon both by email on IRC. The email
is a quick summary to open the discussion.
First, on the "copy stacks" X "link stacks" issue, some of the issues
with copying stacks:
*) We cannot in general inline from C to rust. For example, we cannot
LTO LLVM into rustc. The problem is that a C compiler cannot prove where
a pointer to the stack might be hidden, so it is not safe to move the
stack.
*) The idea of using a special calling convention for doing rust to C
calls only works if we C stack is in a really easy to find place, like a
pinned register. We could do better than we do now by converting the
upcall functions with intrinsic functions, but that is still not ideal.
*) The rust compiler knows what points to the stack, but LLVM has to
keep track of that. This is equivalent to what other languages have to
keep track of for GC and unfortunately LLVM is not very good at it right
now. It only tracks GC roots in memory, which would force us to always
access pointers to the stack via a load of a root.
*) One case I am not sure how to handle is that of a function that takes
a reference argument. That reference could point to the stack, so it has
to go to a GC root, but the check for "do I need more stack space" goes
before we have a chance to store it in a root :-(
Given this and the fact that there is already interest in having LLVM
support linked stacks, for the rest of the email I will assume we will
use stack linking instead of copying.
The way I see it, the big advantaged of tasks would be if they could be
used like erlang threads or goroutines. The programmer can just create
lots of them, use blocking APIs and they get scheduled as needed.
Unfortunately, that model *cannot* be implemented in rust. A task cannot
move from a thread to another, so it is possible for two tasks that
could be executing to be in the same thread.
Consider the example of a browser that wants to fetch many objects and
handle them. It would be very tempting to create one task for each of
the objects, but we cannot do that. The task creation would happen
before the network request and we would be already pinned to a thread
before knowing which resource would be available first.
A similar problem happens for pure IO, like a static http server. Open a
thread per request and you don't know which read will finish first. Some
of this can be avoided by having a clever IO library where read just
sends a message to an IO thread that uses select. Unfortunately, this
will not work when using mmap for example.
For these reasons it looks to me as if tasks add a lot of cost for a
small benefit. My main proposal in this email (other than avoiding the
stack copying implementation) is
--------------------------------------------------------------------
Lets implement just process and threads for now. With these in place we
can see how far we can go. Once we have a need for more abstraction, we
can revisit what a task is and implement it.
--------------------------------------------------------------------
And some different implementation ideas for when we do decide to
implement tasks:
* Use an OS thread of each task. What we currently call a thread in rust
will then just be a control for what tasks can run in parallel. A coarse
and easy to use parallelism that that user can refine if it finds a
contention.
This solves the "task blocking because of unrelated task" problem with
no extra code, even for memory mapped IO.
Another advantage of this implementation is that we can expose any OS
level services to the tasks. For example, we can deliver signals without
having to de multiplex them.
This is not as expensive as it looks, since we would still be using
small stacks. It is hard to image a case where this is too expensive but
the existing proposal is not. If there are cases that do need very light
tasks:
* Go with an even lighter notion of what a task is. The idea is to
implement something like GCD. In the current implementation of GCD (as
in most C code), the burden of safety is always in the programmer. We
can probably do a bit better for rust in the common case.
Thread pools could have ownership of what the tasks can access. In the
case of constant data, that is always safe. In the case of mutable data
they can use some form of exclusion (running in a single thread as the
current tasks or locks) or delegate to the programmer in an unsafe block.
The example of a browser reading images becomes:
* The image loading is done using threads, async IO or tasks. Each image
is fetched, frozen and sent to the pool.
* Each image rendering is a task. They get issued as the images become
available. After this, the programmer has some options:
* Do an unsafe write to the assigned memory position.
* Freeze the rendered image and send it to a thread managing the
final buffer.
* Create a "splat this into the final buffer" task if we are really
into the GCD way.
This is more code than the what would be written with the current tasks,
but at least it behaves as expected. You never get an image that is not
displayed because another one is being slow to load.
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev