-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: IO simplification #219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,332 @@ | ||
- Start Date: (fill me in with today's date, 2014-08-21) | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
|
||
This RFC proposes a significant simplification to the I/O stack distributed with | ||
Rust. It proposes to move green threading into an external Cargo package, and | ||
instead weld `std::io` directly to the native threading model. | ||
|
||
The `std::io` module will remain completely cross-platform. | ||
|
||
# Motivation | ||
|
||
## Where Rust is now | ||
|
||
Rust has gradually migrated from a green threading (lightweight task) model | ||
toward a native threading model: | ||
|
||
* In the green threading (M:N) model, there is no direct correspondence between | ||
a Rust task and a system-level thread. Instead, Rust tasks are managed using a | ||
runtime scheduler that maps them to some small number of | ||
underlying system threads. Blocking I/O operations at the Rust level are | ||
mapped into asyc I/O operations in the runtime system, allowing the green task | ||
scheduler to context switch to another task. | ||
|
||
* In the native threading (1:1) model, a Rust task is equivalent to a | ||
system-level thread. I/O operations can block the underlying thread, and | ||
scheduling is performed entirely by the OS kernel. | ||
|
||
Initially, Rust supported only the green threading model. Later, native | ||
threading was added and ultimately became the default. | ||
|
||
In today's Rust, there is a single I/O API -- `std::io` -- that provides | ||
blocking operations only and works with both threading models. It is even | ||
possible to use both threading models within the same program. | ||
|
||
## The problems | ||
|
||
While the situation described above may sound good in principle, there are | ||
several problems in practice. | ||
|
||
**Forced co-evolution.** With today's design, the green and native | ||
threading models must provide the same I/O API at all times. But | ||
there is functionality that is only appropriate or efficient in one | ||
of the threading models. | ||
|
||
For example, the lightest-weight green threading models are essentially just | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The lack of the ability to context switch means it's not a thread, but rather a true lightweight task (work queues, task graphs / tree). I've never heard closures in a thread pool / task tree referred to as threads. |
||
collections of closures, and do not provide any special I/O support (this | ||
style of green threading is used in Servo, but also shows up in | ||
[java.util.concurrent's exectors](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executors.html) | ||
and [Haskell's par monad](https://hackage.haskell.org/package/monad-par), | ||
among many others). On the other hand, green threading systems designed | ||
explicitly to support I/O may also want to provide low-level access to the | ||
underlying event loop -- an API surface that doesn't make sense for the native | ||
threading model. | ||
|
||
Under the native model we ultimately want to provide non-blocking and/or | ||
asynchronous I/O support. These APIs may involve some platform-specific | ||
abstractions (Posix `select` versus Windows iocp) for maximal performance. But | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Linux uses epoll, and *BSD / OS X use kqueue. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume that this comment was just a "for example". In practice, you'd use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it was just an example. |
||
integrating them cleanly with a green threading model may be difficult or | ||
impossible -- and at the very least, makes it difficult to add them quickly | ||
and seamlessly to the current I/O system. | ||
|
||
In short, the current design couples threading and I/O models together, and | ||
thus forces the green and native models to supply a common I/O interface -- | ||
despite the fact that they are pulling in different directions. | ||
|
||
**Overhead.** The current Rust model allows runtime mixtures of the green and | ||
native models. The implementation achieves this flexibility by using trait | ||
objects to model the entire I/O API. Unfortunately, this flexibility has | ||
several downsides: | ||
|
||
- *Binary sizes*. A significant overhead caused by the trait object design is that | ||
the entire I/O system is included in any binary that statically links to | ||
`libstd`. See | ||
[this comment](https://github.com/rust-lang/rust/issues/10740#issuecomment-31475987) | ||
for more details. | ||
|
||
- *Task-local storage*. The current implementation of task-local storage is | ||
designed to work seamlessly across native and green threads, and its performs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. performs -> performance |
||
substantially suffers as a result. While it is feasible to provide a more | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't actually think it's feasible to have a fast implementation without integration with the linker. Support for loading libraries dynamically (consider |
||
efficient form of "hybrid" TLS that works across models, doing so is *far* | ||
more difficult than simply using native thread-local storage. | ||
|
||
- *Allocation and dynamic dispatch*. With the current design, any invocation of | ||
I/O involves at least dynamic dispatch, and in many cases allocation, due to | ||
the use of trait objects. However, in most cases these costs are trivial when | ||
compared to the cost of actually doing the I/O (or even simply making a | ||
syscall), so they are not strong arguments against the current design. | ||
|
||
**Problematic I/O interactions.** As the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Green threads are fundamentally broken in this sense. If the compiler doesn't insert yields in function preludes and loop edges, it is incredibly easy to block the green thread schedulers. Since there are no plans to do that in the compiler, green threads in Rust are never going to be anything but a toy unsuitable for reliable software. You can't just spin up unknown numbers of threads dynamically in an attempt to cope without completely giving up robustness. It's unrealistic to expect programmers to insert yields everywhere they're needed because even the first step of giving up any hope of using libraries is too much. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I've been working personally on a hack to implement green threads that don't need dynamically inserted yields. What one has is a very slow timer so that most context switches will be purely user-land and fast but for fairness a few will be OS generated signals. Writing async-signal-safe timer signal based context switches is a pain but should be possible to do it by abusing static void handle_fiber_switch_signal(int sig, siginfo_t *info,
void *untyped_ucontextp)
{
struct __ssg_pthread *self;
struct __ssg_pthread *new_thread;
ucontext_t *ucontextp;
ucontextp = (ucontext_t*)untyped_ucontextp;
if (_mylibc_pthread_yield_is_locked()) {
return;
}
self = __mylibc_pthread_current_thread;
new_thread = self->next;
if (self != new_thread) {
self->ucontext = *ucontextp;
#ifdef __linux__
self->ucontext.uc_mcontext.fpregs = &self->ucontext.__fpregs_mem;
#endif
self->saved_errno = errno;
initialize_context(new_thread);
*ucontextp = new_thread->ucontext;
#ifdef __linux__
ucontextp->uc_mcontext.fpregs = &ucontextp->__fpregs_mem;
#endif
}
} This doesn't actually work because Linux rejects the stack frame as invalid and sends a |
||
[documentation for libgreen](http://doc.rust-lang.org/green/#considerations-when-using-libgreen) | ||
explains, only some I/O and synchronization methods work seamlessly across | ||
native and green tasks. For example, any invocation of native code that calls | ||
blocking I/O has the potential to block the worker thread running the green | ||
scheduler. In particular, `std::io` objects created on a native task cannot | ||
safely be used within a green task. Thus, even though `std::io` presents a | ||
unified I/O API for green and native tasks, it is not fully interoperable. | ||
|
||
**Embedding Rust.** When embedding Rust code into other contexts -- whether | ||
calling from C code or embedding in high-level languages -- there is a fair | ||
amount of setup needed to provide the "runtime" infrastructure that `libstd` | ||
relies on. If `libstd` was instead bound to the native threading and I/O | ||
system, the embedding setup would be much simpler. | ||
|
||
**Maintenance burden.** Finally, `libstd` is made somewhat more complex by | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a massive understatement. It adds a substantial amount of complexity, to the point where it takes at least ten times as much code to support anything as it would otherwise. Just look at how complex it is to support mutexes and thread-local storage - there's at least 500x more code than there would be otherwise, and that's not even hyperbole. It's misleading to phrase this as "somewhat more complex". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The TLS code is going away under this proposal. Mutexes are quite simple with park/unpark: the code is even in the LockSupport class mentioned above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because you're still throwing out lock elision and powerful APIs for the concurrency primitives. Rust standard library doesn't even have separate condition variables for the native mutex right now, it's a complete joke. |
||
providing such a flexible threading model. As this RFC will explain, moving to | ||
a strictly native threading model will allow substantial simplification and | ||
reorganization of the structure of Rust's libraries. | ||
|
||
# Detailed design | ||
|
||
To mitigate the above problems, this RFC proposes to tie `std::io` directly to | ||
the native threading model, while moving `libgreen` and its supporting | ||
infrastructure into an external Cargo package with its own I/O API. | ||
|
||
## A more detailed look at today's architecture | ||
|
||
To understand the detailed proposal, it's first necessary to understand how | ||
today's libraries are structured. | ||
|
||
Currently, Rust's runtime and I/O abstraction is provided through `librustrt`, | ||
which is re-exported as `std::rt`: | ||
|
||
* The `Runtime` trait abstracts over the scheduler (via methods like | ||
`deschedule` and `spawn_sibling`) as well as the entire I/O API (via | ||
`local_io`). | ||
|
||
* The `rtio` module provides a number of traits that define the standard I/O | ||
abstraction. | ||
|
||
* The `Task` struct includes a `Runtime` trait object as the dynamic entry point | ||
into the runtime. | ||
|
||
In this setup, `libstd` works directly against the runtime interface. When | ||
invoking an I/O or scheduling operation, it first finds the current `Task`, and | ||
then extracts the `Runtime` trait object to actually perform the operation. | ||
|
||
The actual scheduler and I/O implementations -- `libgreen` and `libnative` -- | ||
then live as crates "above" `libstd`. | ||
|
||
## The near-term plan | ||
|
||
The basic plan is to decouple *task scheduling* from the basic *I/O* | ||
interface: | ||
|
||
- An API for abstracting over schedulers -- the ability to block and wake a | ||
"task" -- will remain available, but as part of `libsync` rather than | ||
`librustrt`. | ||
|
||
- The `std::io` API will be tied directly to native I/O. | ||
|
||
### Tasks versus threads | ||
|
||
In the proposed model, threads and tasks *both* exist and play a role in Rust: | ||
|
||
- Rust code always runs in the context of some (native) thread. We will add | ||
direct support for native thread-local storage, spawning native threads, etc. | ||
|
||
- The `libsync` crate will provide a notion of *task* that supports explicit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this belong in the standard library? The use case for green threads doesn't seem to exist, and adding complexity without a clear reason is a strange proposal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Servo may still want them for I/O—it doesn't want to spawn one native thread per I/O operation—and others may as well. Raw I/O performance doesn't matter so much for Servo as it does for C10K servers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why wouldn't it be using AIO then? Creating a 2-8M memory allocation for each I/O operation isn't good whether it's a green or native thread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it's more convenient to write in a blocking I/O style. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's more convenient to use a high-level AIO API than it is to manage a whole bunch of tasks and deal with message passing. It's also far more efficient and isn't prone to race conditions. Spawning a task only to block it on an I/O operation and then dealing with communication between tasks in order to make it asynchronous is baroque. It's also completely broken in most cases for pipes and sockets. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Totally agree. This has been a pain point for us at Tilde.
Truth.
I don't agree. I assume you're talking about coroutines or async/await? I think macros can help us evolve a good concise solution to this problem, just as they were used in JavaScript to prototype async/await. I could be wrong, but I think that shallow generators could be implemented today in terms of the existing semantics (with some unsafe code).
I agree that we should err on the side of caution due to Semver. We're going to be stuck with anything we ship with 1.0 for a long time, so it's better to ship conservatively than to overreach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think it would be very difficult to make a safe abstraction for this without compiler support. It might be impossible, but I don't know for sure. I do know that the compiler errors are going to be incomprehensible :). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be clear, I didn't mean to imply that macros could express shallow generators directly. Instead, I was trying to say that (as a systems language) Rust could express shallow generators the same way that C can (unsafely, but with a safe interface), and that macros could express async/await on top. I'm not sure if that ends up being true, but I have a hunch that it is 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm assuming you mean like a function that crawls up stack pointers to copy the stack frame to the heap and manually pop the frame. I'm not positive, but this sounds hard to do, at least portably. You'd have to worry about different stack layouts, and also things like preventing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually playing around with doing something like this in a syntax extension, by modifying the AST. There's some precedent here - regenerator rewrites ES6-style generators into pure ES5 by converting them into a state machine, and I suspect that something like this might be possible with Rust too. Though, I also suspect that it'll be much harder than in Javascript due to potential lifetime conflicts, but that's something I'm not really qualified to talk about 😉 Also worth noting that the syntax extension approach doesn't preclude inserting potential yield calls in long-running loops, or before recursive calls, for example. |
||
blocking and waking operations (**NOTE**: this is different from e.g. calling | ||
blocking I/O; it is an explicit request from Rust code to block a task). At | ||
the outset, Rust programs will run in the context of a native task, where | ||
blocking just blocks the underlying thread. But green threading libraries can | ||
introduce their own task implementation, via scoped thread-local storage, | ||
which will allow blocking a green task without blocking the underlying native | ||
worker thread. | ||
|
||
The notion of task and its associated API is described next. | ||
|
||
### Scheduler abstraction | ||
|
||
Above we described numerous problems with trying to couple I/O and threading | ||
models and thereby impose a single I/O model. | ||
|
||
However, concurrency structures built within Rust -- locks, barriers, channels, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If anything dealing with concurrency has to worry about green threads, there's still a big problem. There is no stated use case for this feature and no benefits have been demonstrated. Every benchmark has shown that green threads are entirely useless - why is it sensible to go out of the way to support them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not everything is a C10K-style server, and not everything is Linux. In some cases you may want a blocking I/O programming but may not want to spawn a new OS thread for every I/O resource. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does it have to do with Linux? The blocking style of programming is completely alien on Windows, it has high-level AIO APIs. Spawning a thread (green or native) only to have it block on an I/O request is always going to be a poor way of doing it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AIO APIs aren't as nice to program with. Look at the difference between e.g. Node vs. Go. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand your comparison. Node doesn't use a high-level AIO API, it uses callbacks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The simplicity of the trait doesn't mean much, considering that it's useless. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't know that it's useless. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All evidence we have so far indicates that it's useless and doesn't fit in with the language's niche. On the other hand, there's nothing to back up the design that's proposed here. Where are the performance numbers showing that there's a use case for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We certainly have performance numbers showing that spawning green tasks is faster than spawning native threads. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The only argument that makes sense for inclusion in the stdlib is "we know that it is useful". We don't know that (Not expressing an opinion either way, just pointing out that we should not be using this as a criterion for additions.) |
||
concurrent containers, fork/join and data-parallel frameworks, etc. -- will all | ||
need the ability to block and wake threads/tasks. **NOTE**: this is an | ||
*explicit* request to block in Rust code, rather than as a side-effect of making | ||
a system call. | ||
|
||
This RFC proposes a simple scheduler abstraction, partly inspired by | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Traits introduce a significant amount of complexity / API surface, and I don't think they should be added without a compelling use case. There's going to be extra complexity in both the implementation and documentation for no apparent reason. Adding a trait before the use case exists and is thoroughly understood is the wrong way to go. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Servo may not want to spawn one OS thread per I/O operation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An OS thread isn't significantly heavier than a green one, so what's your point? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't know that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you don't know whether it can ever be successful, it belongs nowhere near the standard library. The complexity is unacceptable unless there's a compelling use case for it, and so far green threads have proven to be useless. The implementation exists and it doesn't accomplish anything despite lots of effort being put into it. Instead of wasting the complexity budget on unproven pet projects, it could be spent on writing a language with support for modern AIO features like resumable functions (generators, coroutines, async/await). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This seems a bit like a self-fulfilling prophecy. Like @Valloric, I find myself agreeing with your technical points quite often. I have even spent a lot of my personal time arguing for improvements to Rust's IO that look a lot like what you want. With enough time and effort, I've had reasonable success persuading people of these things (as a non-Mozillian and non core-team member). This RFC is (perhaps ironically) a step in the direction of our shared goals! Sadly, your tactics are poisoning the technical argument, and making it harder for me (and others) to argue for them effectively. If you're burned out on arguing constructively, just let those of us who share your goals make the case. I think you'll be happy with the outcome. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been quite successful in influencing Rust's development with a diplomatic approach. My almost entirely negative attitude towards the project has everything to do with how I've been treated and nothing to do with being burned out on arguing constructively. If I wanted to waste countless more hours of my time trying to steer the project towards success, then I wouldn't be speaking my mind. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thestinger I'm a little confused by your last comment. Why would your time be wasted trying to suggest improvements to Rust if you've had success in the past? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only thing I got in return for voicing my opinions on design was a hostile environment where every single attempt at making a contribution meets harsh resistance. It used to be a friendly environment until I decided to voice a dissenting opinion on the design of I/O / concurrency a year ago and it was taken as a personal attack by the people who wrote the runtime. Since then, my pull requests and opinions have become progressively less welcome. Rust is a project where who you are matters far more than the merit of your ideas. It ends up arriving at the right design choices only after ignoring reality and trying every other option. I can't submit a single pull request under my real name without barriers being thrown up and it rotting for ages, and any attempt at an RFC will just be rejected with no justification or taken out of my hands and implemented without giving me any role in the design process or credit for the work I put into it. I know that contributing to Rust is a waste of time when I need to submit any non-bugfix pull request under a pseudonym if I actually want it to be merged. I now see my involvement with the project in a negative light in the sense that I've spent countless hours of unpaid labour working on a project where I'm no longer welcome. Since that's how I see things, I've chosen to express my negative feelings in my comments for the past months. If I just wanted to pour even more of my time into the project for the largest possible gains, then I wouldn't be putting forward my unfiltered opinions in a negative tone. Instead, I've chosen to keep contributing on a much smaller scale and I haven't made any attempt to hide how I feel about the project. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've been actively following the Rust project for over a year and while I on occasion disagree with some of the decisions by members of the core team, I've found them all to be incredibly reasonable, intelligent and polite, without fail or exception. In general, I've found it incredibly easy to misunderstand people's intentions, opinions and reactions when interacting with them online. It's absurd how often this happens, even when I'm on the lookout for it. I'm sure that's what happened in your case in both directions. You have a tendency to present a strong, unfiltered opinion that those unaccustomed to such an approach might find harsh. Put that together with everyone's tendency to misunderstand others online and you have a "personal attack."
I strongly doubt that's the case. You are very knowledgeable and technically competent and the Rust team isn't stupid by any means so I'm sure they value your contributions (which like you mentioned are free). I'm also sure you believe you are less welcome than you actually are.
IMO that overstates it quite a bit, but I'll agree that there's far more truth to that statement than there should be. The core team does have a slight tendency to do go with the (sometimes obviously) right design only after exercising all other options first.
This isn't true, and so aren't your other comments that the RFC process is a rubber-stamp for ideas from the core team. The RFC's here are proof enough. Plenty of core team RFC's fail after pushback from the community. This happens all the time and is exactly as it should be. Also, plenty of RFC's from non-core team people are accepted. It's certainly easier to convince someone that your idea has merit when you can talk to them in person instead of just over IRC or GitHub and that contributes to what you perceive. But that's just human nature.
I doubt this is true. I think you may be referencing the tail call RFC you submitted. IMO that was a great RFC that should have been accepted, but I also see the core team's concern of "we just want to ship 1.0 ASAP and this isn't critical to that so no." I don't like that position but I do understand it. I also think that the way the core team handled that from a communication standpoint wasn't great, but AFAIK some of the RFC discussion process was changed as a result of that (we now know ahead of time which RFC's will be discussed at a core team meeting). IIRC you were unhappy that this RFC was rejected without your input and that the work you put into implementing it was wasted (correct me if I'm wrong). Please realize that a large part of the blame for the wasted work does lie with you; you decided to start implementing it before anyone decided to accept the RFC. That was IMO a mistake on your end. I also didn't see anything related to that RFC (in the meeting minutes, the RFC or IRC) that would indicate that any personal dislike for you was a factor in the decision. Frankly I find the idea of this to be absurd. Daniel, you must realize that you're creating a vicious cycle here. Because you think you're unwelcome, you frequently comment with an "everything here sucks" attitude which in turn and over time could easily make you unwelcome to some, thus creating the situation you dislike. I recommend taking a step back and giving Rust and the core team a clean slate. You must also try to moderate your approach; you mention that it isn't your intent to convince others and yet that's precisely what you must achieve if you want to change any aspects of Rust's design. I wouldn't presume to speak for the entire Rust community, but I personally highly value your input. But if you couch it in a "everything here sucks" attitude it will fail to provide value and will only be detrimental, both to Rust and to your personal happiness. Life's too short to hold grudges. Your current attitude isn't useful, to you or anyone else. Forgive, forget, move on and continue to provide valuable feedback to Rust's design in a detached and diplomatic manner. |
||
[java.util.concurrent](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/LockSupport.html) | ||
and partly by our current runtime infrastructure. Concurrency structures that | ||
use this abstraction can be used freely under multiple threading models at the | ||
same time. Here is a *sketch* of the API, which will need some experimentation | ||
before nailing down in full detail: | ||
|
||
```rust | ||
// details TBD, but WakeupHandle: Send + Clone | ||
type WakeupHandle = ...; | ||
|
||
impl WakeupHandle { | ||
/// Attempt to wake up the task connected to this handle. | ||
/// | ||
/// Each `WakupHandle` is associated with a particular invocation of | ||
/// `block_after`; only one call to `wake` will take effect per invocation. | ||
/// Returns `true` if `wake` actually woke up the task. | ||
/// | ||
/// Note that the task may no longer exist by the time `wake` is invoked. | ||
fn wake(self) -> bool; | ||
} | ||
|
||
trait Task { | ||
/// Give up the current timeslice. | ||
fn yield_now(&self); | ||
|
||
/// Blocks the current task after executing the callback `f`. | ||
/// | ||
/// Blocking can be canceled by using `wake` on the `WakeupHandle`. | ||
fn block_after(&self, f: |WakeupHandle|); | ||
} | ||
|
||
/// Get access to scheduling operations on the current task. | ||
fn cur_task(|&Task|); | ||
``` | ||
|
||
The above API will be exported in `libsync`. The idea is that `cur_task` reads | ||
from a dynamically-scoped thread-local variable to get a handle to a `Task` | ||
implementation. By default, that implementation will equate "task" and "thread", | ||
blocking and waking the underlying native thread. But green threading libraries | ||
can run code with an updated task that hooks into their scheduling infrastructure. | ||
|
||
To build a synchronization construct like blocking channels, you use the | ||
`block_after` method. That method invokes a callback with a *wakeup handle*, | ||
which is `Send` and `Clone`, and can be used to wake up the task. The task will | ||
block after the callback finishes execution, but the wakeup handle can be used | ||
to abort blocking. | ||
|
||
For example, when attempting to receive from an empty channel, you would use | ||
`block_after` to get a wakeup handle, and the store that handle within the | ||
channel so that future senders can wake up the receiver. After storing the | ||
handle, however, the receiver's callback for `block_after` must check that no | ||
messages arrived in the meantime, canceling blocking if they have. | ||
|
||
The API is designed to avoid spurious wakeups by tying wakeup handles to | ||
specific `block_after` invocations, which is an improvement over the | ||
java.util.concurrent API. | ||
|
||
A key point with the design is that wakeup handles are abstracted over the | ||
actual scheduler being used, which means that for example a blocked green task | ||
can safely be woken by a native task. While the exact definition of a wakeup | ||
handle still needs to be worked out, it will contain a trait object so that the | ||
`wake` method will dispatch to the scheduler that created the handle. | ||
|
||
### `std::io` and native threading | ||
|
||
The plan is to entirely remove `librustrt`, including all of the traits. | ||
The abstraction layers will then become: | ||
|
||
- Highest level: `libstd`, providing cross-platform, high-level I/O and | ||
scheduling abstractions. The crate will depend on `libnative` (the opposite | ||
of today's situation). | ||
|
||
- Mid-level: `libnative`, providing a cross-platform Rust interface for I/O and | ||
scheduling. The API will be relatively low-level, compared to `libstd`. The | ||
crate will depend on `libsys`. | ||
|
||
- Low-level: `libsys` (renamed from `liblibc`), providing platform-specific Rust | ||
bindings to system C APIs. | ||
|
||
In this scheme, the actual API of `libstd` will not change significantly. But | ||
its implementation will invoke functions in `libnative` directly, rather than | ||
going through a trait object. | ||
|
||
A goal of this work is to minimize the complexity of embedding Rust code in | ||
other contexts. It is not yet clear what the final embedding API will look like. | ||
|
||
### Green threading | ||
|
||
Despite tying `libstd` to native threading, however, green threading will still | ||
be supported. The infrastructure in `libgreen` and friends will move into its | ||
own Cargo package. | ||
|
||
Initially, the green threading package will support essentially the same | ||
interface it does today; there are no immediate plans to change its API, since | ||
the focus will be on first improving the native threading API. Note, however, | ||
that the I/O API will be exposed separately within `libgreen`, as opposed to the | ||
current exposure through `std::io`. | ||
|
||
The library will be maintained to track Rust's development, and may ultimately | ||
undergo significant new development; see "The long-term plan" below. | ||
|
||
## The long-term plan | ||
|
||
Ultimately, a large motivation for the proposed refactoring is to allow the APIs | ||
for native and green threading and I/O to grow and diverge. | ||
|
||
In particular, over time we should expose more of the underlying system | ||
capabilities under the native threading model. Whenever possible, these | ||
capabilities should be provided at the `libstd` level -- the highest level of | ||
cross-platform abstraction. However, an important goal is also to provide | ||
nonblocking and/or asynchronous I/O, for which system APIs differ greatly (Posix | ||
`select` versus Windows `iocp`). It may be necessary to provide additional, | ||
platform-specific crates to expose this functionality. Ideally, these crates | ||
would interoperate smoothly with `libstd`, so that for example a `libposix` | ||
crate would allow using a `select` operation directly against a | ||
`std::io::fs::File` value. | ||
|
||
We may also wish to expose "lowering" operations in `libstd` -- APIs that allow | ||
you to get at the file descriptor underlying a `std::io::fs::File`, for example. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's important that these kind of details are exposed - there are a few places where I am, for example, transmuting a custom structure into a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed fully. I think it's critical that we expose lowering operations to avoid fragmenting the ecosystem into "high level" and "low level" APIs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to use C Standard There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be nothing in the I/O libraries based on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. |
||
Finally, there is a lot of room to evolve `libgreen` by exposing more of the | ||
underlying event loop functionality. At the same time, it is probably worthwhile | ||
to build an alternative, "very lightweight" green threading library that does | ||
not provide any event loop or I/O support -- the "green threads" are essentially | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not really a thread if it doesn't support context switching on I/O. At that point, it's a work queue / task tree / task graph. Queues of closures without any support for context switching isn't threading. |
||
just closures. Servo already makes use of such a model in some places internally. | ||
|
||
All of the above long-term plans will require substantial new design and | ||
implementation work, and the specifics are out of scope for this RFC. The main | ||
point, though, is that the refactoring proposed by this RFC will make it much | ||
more plausible to carry out such work. | ||
|
||
# Drawbacks | ||
|
||
The main drawback of this proposal is that green I/O will be provided by a | ||
forked interface of `std::io`. This change makes green threading feel a bit | ||
"second class", and means there's more to learn when using both models | ||
together. | ||
|
||
This setup also somewhat increases the risk of invoking native blocking I/O on a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any kind of loop / calculation can block the scheduler. It's already broken today because the compiler doesn't insert yields and green threads are never going to be viable without that compiler support. Programmers are not capable of manually inserting yield checks in all the loops where it's important, especially considering that libraries exist. |
||
green thread -- though of course that risk is very much present today. One way | ||
of mitigating this risk in general is the Java executor approach, where the | ||
native "worker" threads that are executing the green thread scheduler are | ||
monitored for blocking, and new worker threads are spun up as needed. | ||
|
||
# Unresolved questions | ||
|
||
There are may unresolved questions about the exact details of the refactoring, | ||
but these are considered implementation details since the `libstd` interface | ||
itself will not substantially change as part of this RFC. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An additional problem which isn't mentioned here is lack of extensibility - if the functionality you want to use does not exist yet, you must modify libnative/librustuv/librustrt to add the functionality (if you want your code to play nicely with other peoples). There's no way to extend it outwith those crates, unless you want it to only work with the native backend. This is a problem I've faced when wanting to add support for using custom file descriptors/sockets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rust's green threads don't even support libraries with loops or recursion. There's not much point in worrying about usage of blocking system calls as that's not nearly as common.