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

Reply via email to