Elementary default runtime

References: https://rust-lang.github.io/wg-async/vision/unresolved_questions/default_runtime.html https://rust-lang.github.io/wg-async/vision/shiny_future/users_manual.html

https://www.reddit.com/r/rust/comments/i4759z/where_are_we_heading_regarding_runtimes/

It is really bad when a valid program in a language doesn't compile (let alone run) without (un-/standard) dependencies. Then, not being able to introduce async/await without "yes, it does not even compile: please add this before main(), please add this to the .toml, I will talk about these later" is not encouraging.

Proposal is to pick:

  1. simply define "async" to nothing and "await" to actually-execute-the-function-here, basically turning async/await into "defer" (aka the .Net strategy); or
  2. get inspired by JS and follow the event loop

The other point is the runtime incompatibility. I/Os appear to be the main culprit there. Adding them to the std?

TBH, while it may appear a bit odd at first, it's never really been a problem for me. When I'm using async I know about it and its consequences, and to assume the same holds true in general wouldn't be outlandish.

2 Likes

I think it would make sense to include a minimal async runtime (basically pollster::block_on) in std, because there is more or less only one way to write that, and it solves the problem of "I need to run some generic code that is designed to permit async components and is therefore async itself, but I am not doing anything really interesting with the async".

Multithreaded async, and async IO runtimes, are much bigger design spaces and therefore much less good candidates for std.

23 Likes

block_on was attempted to be added to std a few years ago

Conclusion from that was "needs RFC", (because it turns out there's not just one way to write it). There's a wg-async team issue about writing such an RFC, but no movement on it that I can see

2 Likes

Yes, we should absolutely have one. We're going to need to finish the async traits in the standard library first, though.

1 Like

Do you mean async fn in traits, or adding specific traits? If so, which ones?

Among other things, I think before we add a runtime to the standard library, we're going to need a trait AsyncRuntime or similar. That's leaving aside the async read/write/etc traits.

While async traits are certainly helpful in designing a block_on for std, I think they're far from necessary. Imho block_on should be the minimal single-threaded executor[1]. Even if we get a standard interface spawn, I don't think block_on should interact with it in any way. Even if std provides a default executor with a thread pool. (And for that reason, putting it on the trait is too aggressive; it should be a free function in the task module imho.)


  1. I am far from fully aware of the options here, but roughly: poll the future, park the thread if it's pending, and the waker unparks the thread. The use of a condvar/futex/etc beyond just thread notification is a potential implementation choice, but a) not particularly relevant to the interface, and b) scoped threads currently just use standard thread parking despite already necessarily having an atomic count it could futex/notify/wait on. ↩︎

1 Like

Personally I think both should be free functions, and then we can add a mechanism for changing the backend they use via a trait or traits.

Agree on both being free functions, but if global[1] spawn is pluggable (it should be imho), block_on really doesn't. If we have context-relative spawn, it could matter, but if we don't and downstream futures use the global spawn, it basically doesn't matter which loop is polling the main driver future. For the case where you really do want it on the pooled executor, you can always block_on(spawn(task)).

Having a guaranteed-simple-single-threaded block_on is I think a benefit even when a potentially-pooled spawn is available[2]. In a way, it's similar to the "now_or_never" "executor," in that its existence is to turn nominally-async code into nominally-sync code in the trivial manner (i.e. use the same underlying syscalls, etc. but userland block waiting for them rather than yield the thread to another task).


  1. I'm a believer in passing more context down on the waker/context, although that'll require new API to enable and for that context not to be lost when wrapping context/waker. (Give us the Provide API for carrying arbitrary executor/etc info please!) ↩︎

  2. Under such a model, a simple pooled executor can just be for _ in 0..n { thread_scope.spawn(|| loop { block_on(tasks.steal()?)?; }); }. I don't have any idea how achievable it is, but I like the vision of the future where the reactor driving future progress is itself another future on the executor. ↩︎

2 Likes

(NOT A CONTRIBUTION)

Block on does not need to be tied to "global executor" stuff or anything like that, and probably shouldn't be. There should definitely be a simple single threaded non-concurrent executor in std, with std-defined behavior, that doesn't depend on choosing a runtime. There is no reason this hasn't happened except lack of drive.

6 Likes

In the OCaml world, we were faced with similar difficulties and we ended up with a "OCaml Batteries Included" distribution, which was meant to be easier to start with for beginners. I've left the OCaml world a few years ago after a burnout, so I'm not sure how things are going these days, but there was lots of enthusiasm at the time for this.

Actually, come to think of it, I think that the fact that OCaml now has a working package manager has probably decreased the need for a Batteries Included distro.

I've long said I'd be happier with a much thinner std, given how good traits and cargo are. You don't need fs, most of sync, most of the collections, just about all intrinsic methods...

This isn't a call to remove things, but it's such a low bar to add things to your dependencies that I don't know what the real benefit would be, urge than perhaps discoverability. The special place std has is knowledge and lockstep updates with the compiler, and being able to escape the orphan rules due to being at the bottom of the dependancy stack, and being a shared vocabulary crate is a nice effect too.

Discoverablity is also pretty easily addressed, by warnings recommending crates, too.

1 Like

There’s also provenance: I trust the compiler developers and code review process enough to let their software run on my computers, but that trust doesn’t necessarily extend to third-party crates— Even those that are written by people who are also Rust compiler developers. In practice, adding additional dependencies to the project can have significant soft costs beyond the mechanics of cargo add, such as evaluating the implementation quality (either by developer reputation or code review), staying on top of updates/security issues, etc.

3 Likes

I think that's a cultural issue far more than a technical issue. We trust std more than say, rustcrypto crates because... why exactly?

Deno decided to not bless their standard library, and there's no provenance issue there.

Speaking only for myself, I trust std because it's developed in lockstep with the compiler by essentially the same team and processes. I had to investigate the quality-control processes used for this project before trusting the compiler, and that investigation necessarily covered std as well. I haven't yet done that for rustcrypto, and so distrust it by default— I have no first-hand evidence either way.

Mentally, I have 3 trust levels for code I didn't write, and it takes a significant investment of my time to elevate something from one trust level to the next:

  • Level 0: Untrusted. Worth consulting for ideas, but not allowed to run on my computers.
  • Level 1: Trusted implementation. I have done at least a cursory code review or other quality assessment, and am willing to run the specific version I've looked at. Most third-party crates I use fall into this category, and get pinned to the version I looked at.
  • Level 2: Trusted process. In addition to looking at the quality of a specific version, I've determined that the project governance can be trusted to maintain that quality and keep out malicious code. This is the category that rustc and std fall into.

If parts of std were split out into separate projects, those new projects would be downgraded to trust level 1 until I got a feel for how the new governance structure operates in practice.

1 Like

IMO one of the problems with saying stuff should be added to std because it's more trusted is that one of the main reasons it can be that trusted is that it is small. Tokio, just the main repo not including dependencies developed by the same project but in other repos, is approximately the same size as std (65kSLoC for tokio vs 79kSLoC for std). If larger projects such as this get merged into std that would necessitate increasing the maintenance team (which is already not able to keep up) diluting the trust inherent in having a small set of known developers.

5 Likes

I don't disagree, but there's nothing special about std there, nor would it involve more work for you to review it to get it to your level 2 were it hosted on crates.io (for example). I mentioned rustcrypto as a project specifically because their governance and procedure is as if not more strict than std (necessarily), to demonstrate there's only a cultural or even only a psychological difference to provenance.

But I don't want to derail this thread too much: the main point is I feel that encouragement for new users to use (popular, well reviewed) crates is a good thing for rust, in general, especially those coming from C or C++ backgrounds where using a library is a huge pain in the butt.

3 Likes

First things first: below Rust only opcodes shall remain. Even ASM (&DSL) shall find its place in Rust. Rust has to stick to bare metal, at all costs. This is paramount.

The proposal (v1) is not about a minimal runtime, but precisely about a zero-cost runtime.

A zero-cost runtime is in fact a compiler abstraction, which in this case projects a multi-threaded async/await program onto a single-threaded async/await execution system. For analogy, think std::unique_ptr.

This introduces no dependencies into Rust programs, just in the compiler (that is, one more module to write). It is what .Net did when got async/await in C(from F)#, and IIUC it is the same thing that @withoutboats & @kpreid propose (it is also the right idea).

It is a proposal intended to plug the first gap in the async/await Rust eco-system. De facto, there are two Rust versions now, out of which one only compiles on systems able to multi-thread. This is an already existing dependency in the language and should be avoided at all costs.

The second gap to be plugged is exactly the incompatible divergence between the async runtimes themselves (e.g. tokio vs. async-std vs...), which shall not be allowed to go beyond test-and-prove concepts. Each of these runtimes already creates its own eco-system (and on top of that each runtime kinda rewrites the entire std to its own image). A standardized mechanism of compatibility between these eco-systems/runtimes is necessary.

The gap(s) may appear small at this time, but there are precedents: D programming language knew the war between the two roses named "phobos" and "tango", which were competing for the status of the-one-true-standard-library, which plagued the project beyond immagination. It took D2, almost a new language, to stop that, and even that only because the "tango" team gave up. Similar fates are to be avoided in a (solid) Rusty world.

One more thing: the as of yet most under-appreciated quality of Rust is not its safety, but its explainability: it codes an intent unambiguously (parenthesis: the post about the alternative proposal of using unique/multiple references instead of mutable/immutable references saw that correctly towards its last paragraphs, and in a way acknowledged that mutable/immutable is the better choice in the end 1). Coding the intent is paramount for the future, when codebases will be gigantesque and all the (intelligent?) tools that will analyze (and, for sure, write) that code will rely essentially on this markup of the intent. It is this quality that makes Rust the first (and maybe the single-ever-needed) universal programming language (UPL): able to cover everything from the bare metal to humongous systems in an explainable way. C did not make it there not because of its lack of expressiveness, but precisely because of its lack of unambiguous intent/explainability. Ditto, C++. Other languages did not touch the bare metal, so "why bother?".

As of now, the areas still restricted to Rust remain the browser (because wasm is still purposefully restricted) and the scripting (getting a bit Rustic at times...). As for universality, the mutable value semantics of Val are the strongest competing idea, a paradigm that Rust itself might (have to) integrate at a future moment.

1 Like

The distrust of non-std crates ends up mixing concerns:

"I need to trust security and quality of the code I use" → "there should be an API, possibly with shared global state, spawning numerous threads, bundled with nearly every Rust program".

I understand the difficulty with vetting crates.io code, but bundling everything into std instead creates lots of side effects.

  • std has very limited ability to evolve APIs due to being forever 1.x version,
  • std implementations are basic or one-size-fits-all, which isn't always desirable, especially for something complex like an async runtime which affects performance and stability of servers.
  • std is bundled with most programs, increasing executable sizes. LTO is slow and doesn't always eliminate everything. When dependencies actually use the std runtime, users of other runtimes will end up with two runtimes.
  • std comes precompiled and doesn't respect all of the build settings.
  • std affects portability of Rust, and already not all of it makes sense in environments like browser WASM.

Currently lack of a built-in runtime encourages libraries to return futures instead of spawning them themselves. This is actually good for ability to control how the futures are executed, prioritizing/deferring their execution, ensuring prompt cancellation, etc.

If there's a need for an official Rust async runtime, this could be done without it being in std. The Rust org could release or bless some async runtime crate. crates.io could have special display for "rust official" crates. Maybe even the Rust installation could come with a bundled crate (like the test crate exists on nightly). All this can be done without making std heavier and undermining applications' control over async execution.

5 Likes