What is the future goal of async Rust?

I come from Go and I'm relatively new to Rust. While I love the language, I really really miss Goroutines and the simplicity. Parallel programming just does not feel the same in Rust. At the same time, I'm hearing a lot of dissatisfaction from the community about async Rust, but I don't have enough understanding of compilers to understand the implications of the designs or what the discussions are about.

Still, I would love to understand async Rust's eventual goal from a user's perspective. Will Rust ever implement something synonymous to Goroutines that hides away the intricacies of async Rust? If not, what can I expect instead?

2 Likes

In some sense, the thing that makes Rust different from Goroutines isn't its async code. Rather, it is the fact that Rust has non-async code too.

The classic analogy with function colors is quite relevant here. The argument is that it is a shame that Rust introduced async/await because it introduces this distinction between non-async and async code that complicates things. However, the way you get a language like Go is not by getting rid of async code — rather, you do it by making everything async.

So in that sense, it's not that there is some third concept of "Rust goroutines" that we can introduce. Because to get to that point, you would have to get rid of non-async code, and then simplify async code in the ways that this allows (e.g. once you no longer have non-async code, you can probably do away with the .await keyword).

We're not going to remove the non-async part of the language. So no, we won't get something closer to goroutines.

To see one place where the fact that Go is secretly async everywhere becomes visible, compare this blog post about async Rust with this old Go issue. I believe that if you read both, the analogy should become clearer, but I'm happy to elaborate if not.

37 Likes

wg-async: How using async Rust ought to feel (and why it doesn't today)

5 Likes

The Rust 2024 roadmap includes improvement to async that are part of the overall vision for improving async. In that last link they talk about making it simpler, which is what I think you're asking about:

  • :sun_behind_small_cloud: If you know sync Rust, getting started in Async Rust is straightforward (more])
  • :sun_behind_small_cloud: Mostly, you change fn to async fn, add some calls to await, and change over to other parts of the stdlib, though supporting dyn Trait requires making some choices, particularly in a no-std environment
  • :sun_behind_small_cloud: It still has that "if it compiles, it generally works, and it runs pretty darn fast" feeling
  • :sun_behind_small_cloud: Destructors and cleanup also work the same way as in sync Rust, thanks to Drop to AsyncDrop
  • :sun_behind_small_cloud: No need to write poll functions or to interact with pin except in quite specialized scenarios

I'm looking forward to this but I expect it to take some time.

6 Likes

What I really would love, and I think it's mentioned in the post @vague linked, is the ability to mix and match crates without needing to think about which one supports async. I want to just tag a function that I'm trying to call as async.

And I would also love it if I could just ignore the runtime. Right now I have to interact with tokio and all of its intricacies. I would love native async runtime support (or something like that) that lets me run async Rust without having to think about which tokio functions I need to call.

I understand there is a fundamental difference between the design of Go and Rust, but are these item on the agenda and are they likely to be implemented?

1 Like

Those items sounds very nice, but also kind of "vague" as in they sound like "call to actions" without concrete plans, but then again I don't know anything about compilers or the Rust-lang devs.

It seems like you are mainly asking for two things:

The ability to use non-async blocking code from async code without having to explicitly mark your code as blocking. I'm quite certain that this will never happen. Please see this article that I linked previously for how to correctly call non-async code from async code.

The ability to mix code that uses different runtimes (or just eliminate the concept of different runtimes). Well, the concept of having different runtimes for different use-cases will not go away. There are people who advocate for having a default runtime, and there are also many people who want to make more libraries runtime-agnostic so that they do not depend on the runtime. So we will most likely see changes in this direction in the future.

However, these things take time. Changes in the async space have been incredibly slow. Async/await was introduced 4 years ago, and I don't think any significant changes related to async/await have happened since then. The first such significant change will be the introduction of async-functions-in-traits, which will be part of the upcoming 1.75.0 release. Almost all of the innovation has happened in libraries, not in the language.

23 Likes

Understandable.

Is there a way, theoretically, to make a preemptive version of the Rust scheduler? and make it an optional feature you'd opt into for your program? Or is that inherently impossible for Rust because there's no way to make it safe?

Do you mean the items I copy pasted into the post? Those are from the "shiny future" section, so yes they are the high level goals. The next section, Roadmap, has the (related) concrete changes planned. Did you see that section?

But the responses from @alice are the best answers to your more specific questions.

1 Like

It's easy to preemptively execute both blocking and async tasks.

std::thread::spawn(|| my_blocking_fn(...));
std::thread::spawn(|| pollster::block_on(my_async_fn(...)));

There, done. (pollster is from here.) If they need to talk to each other, add some channels. And in this situation, it won't even hurt anything if my_async_fn() executes some blocking code inside itself — because the task has a dedicated thread, it can't be starving anything else.

Of course, you may object, this doesn't have any of the benefits of async or goroutines. True. But in order to execute blocking code concurrently, you need a thread — otherwise the code may have its reasonable expectations about things like thread-local variables violated.

The more serious problem is that this doesn't grant any of the non-performance benefits of the async paradigm — like cancellability. It's fundamentally impossible to cancel an arbitrary function/thread[1], but futures are always cancellable at every suspend point, so you can do things like select! and other future combinators that add cancellation. So, while it can be sometimes okay for an async function to call a blocking function, it can't be generally okay, because it breaks callers' assumptions about how a future will behave.


  1. Unless you're implementing a “virtual machine” type system where you control 100% of the machine code that thread runs and you have a plan for what to do if that thread is holding a lock when it's cancelled. As far as I've heard, Erlang is the only language that has done this, and Rust definitely won't ever because it supports calling into foreign code. ↩︎

14 Likes

I don't think this is representative. I am perfectly happy with async Rust as it is, which is great. It did everything I expected it to do perfectly. ( See here for some async functions I wrote ]

[ You might encounter puzzling error messages, at which point you need to understand things like Send and Sync ]

6 Likes

Keyword generics look like they could be part of the plan to reduce friction when switching to async code. The points above suggest async I/O could be added to the standard library as well. I don't know if an entire async runtime would ever be added to the standard library though.

Theoretically it's possible.

I sincerely hope that would never happen.

It's impossible to make it robust.

I think the whole discussion which goes around year after year is about “magic”.

Essentially all popular languages in use today are full of “magic”. Memory is magically handled by tracing GC, strings are handled by another magical subsystem, all the code in Go is magically made interruptible via yet another flavor of magic and so on.

And the fundamental property of all that magic is that it just doesn't work.

And while people often argue about “Rust way” of doing things (whether it's explicit or not, etc)… the actual rule that Rust development follows is: Rust doesn't believe in magic.

I think that's the simplest formulation of all-encompassing principle: no magic at runtime. It's as simple as that.

Rust have tons of magic. Type inference, lifetime calculations, tricky HRBT rules and so on.

But under all that… the guiding idea of Rust is: magic is fallible and thus it must only be used in compile time.

I'm not sure if anyone ever planned to make Rust an anti-magical language, but that's how it works today: most constructs which are rejected are rejected “because they are not explicit enough”, but in reality they are rejected because they add potentially fallible, hidden, runtime magic to Rust.

That's how Rust become the hope of the embedded world (which is close to the metal and with consequences of failure so dire they just simply couldn't afford magic), that's how it become most loved language for many years in row… and that's how it become language where desires like these discussed in that thread have no hope of becoming reality:

That's plea for more magic.

That's request for even more magic.

Something that may help you, the developer to solve the issues. As I have said Rust includes tons of magic and is not shy of adding more.

The only catch: all that magic belongs to compile-time, not runtime!

You probably would get traits that would allow you not to think about which tokio functions you need to call — but you would think about which standard library function you'll need to call, instead.

You may get some more syntax sugar, but when it would break — it would be your responsibility to take both pieces and make it whole.

On one hand this makes it possible to be sure that program you wrote is actually working, reliably, not by accident or quirk of local configuration.

On the other hand it means that if something doesn't work — it's your fault and you would never get that permission not to think about some of these things that you want to ignore while Rust runtime would magically resolve issues for you.

18 Likes

I rather hope that does not happen. To make a preemptive scheduler part of the Rust language would require that it had a scheduler in the first place. That would mean that Rust would have a "run time". That is to say code that runs behind the scenes to manage threads and such. That is to say a big lot of code running that I did not actually order when I wrote my Rust source code.

That to my mind is totally against the idea of Rust being a "systems programming language". Where I take systems programming language as meaning a language that one can use to write operating systems, schedulers, memory management systems, garbage collectors and so on. As one can do in languages like C. This is in opposition to being a language that depends on already having those things built in.

It would also likely prevent use of Rust on micro-controllers and other small systems.

In short nothing is impossible but not everything is desirable for the intended purposes of the language. I read that Rust did actually already include an implementation of "green threads" that as far as I understand pretty much did what you are asking for. That implementation was removed as it was contrary to the systems programming aims of Rust.

3 Likes

Why doesn't the "function colors" argument apply to any language with types? Couldn't someone ask "What color is your integer" since Rust has so many integer types? There is a way out of that too and it's to disregard performance and make all integers heap-allocated and arbitrary precision.

3 Likes

I have been wondering about that colored function thing ever since it came out.

Of course the analogy of red and blue functions (or whatever the colours were) makes it sound very undesirable to have such a situation. Of course as being red or blue serves no useful purpose it's an absurd imposition.

But how good an analogy to sync and async functions is it?

Seems to me that sync and async code live in very different worlds. "normal" synchronous code requires no run-time from the language. Single threaded code needs no scheduler at all and threaded code uses whatever scheduling is provided by the operating system on which it runs.

Meanwhile async code does require a run time. Async code is compiled to very different machine code.

As such I think calling for the distinction between sync and async functions in our source code is justified and even desirable. Trying to hide such a big fundamental differences under some kind of abstractions that try to make them invisible seems very unlikely to work well, to be leaky.

This is very different from the arbitrary imposition of red and blue functions for no useful purpose.

8 Likes

Yeah, to be clear, I don't think "function color" is a problem; I appreciate that the well-typedness of my programs guarantees that they don't exhibit some bugs.

I haven't used Go since generics were added but I found interface {} quite painful. It's certainly convenient to have everything async. The world is big enough for both languages; I prefer the safety guarantees Rust has with Send etc.

1 Like

I think what makes function colors annoying is how difficult it is to move between them. For integers, you can just use a cast.

2 Likes

I guess it is a bit clunky. But thanks to your help Alice I managed to mix up async and blocking code in an application when I was still a rank beginner at Rust. It's still running in production without issue three years later.

Thanks again.

2 Likes

It applies, 100%. Absolutely.

One may ask and there's very easy answer to that: generics. They may work with any integer type, if properly written.

There are no such answer to sync/async function but lots of people feel there should be.

Nope, there are other “ways out”.

It's not entirely true. Lots of algorithms don't care about what you are writing, sync or async. Similarly to how lots of algorithms are applicable to integers of all sizes.

Not really. Internally async code is compiled in precisely the same fashion as sync code, only instead of using rsp pointer to the function frame it uses some other pointer for that task.

That's the only difference, ultimately.

I don't think anyone plans to do that. Rust is not Go. But being able to write universal functions which may be instantiated as red or blue may be quite useful.

Since async functions are, basically, having two stack frames (one is permanent, one is borrowed when they are not awaiting) it should be possible to write maybe-async functions which can be instantiated as sync or async ones.

1 Like