Earlier quoted context omitted.
How's that play out in terms of, say, performing some long-running calculations, rather than something that performs IO?
You're essentially implementing statically scheduled cooperative userspace threads on top of a single thread with async/await. So, for computation, this is a bad solution.
How Rust optimizes async/await
101–110 of 130 posts
Re: How Rust optimizes async/await
#102Does anyone know how Rust's implementation compares to C++2a's? The C++ people seem to have spent a lot of time creating an extremely generic framework for async/await wherein it is easy to change out how the scheduler works (I currently have a trivial work stack, but am going to be moving to something more akin to a deadline scheduler in the near future for a large codebase I am working with, which needs to be able…
Disclaimer: I'm not an expert on the proposal, but have looked at it some, and can offer my impressions here. (Sorry, this got a bit long!) The C++ proposal definitely attacks the problem from a different angle than Rust. One somewhat surface-level difference is that it implements co_yield in terms of co_await, which is the opposite of Rust implementing await in terms of yield. Another difference is that in Rust, all…
I don't think this is correct. C++ 20 allows a lot of choices to implement it without forcing a heap allocation.
see https://lewissbaker.github.io/2017/11/17/understanding-opera... also see this video that goes in depth how to have billions of coroutines with C++: https://www.youtube.com/watch?v=j9tlJAqMV7U
Re: How Rust optimizes async/await
#103How does `yield` work under the hood? Does it add a reference to some array, with code to loop over all the references with some small timeout until the reference status changes from "pending" to "completed"?
|| {
yield 2;
yield 3;
yield 5;
}
will get converted to a struct that implements the Generator trait[1] with a resume method something like fn resume(self: Pin) -> GeneratorState {
match self.next_state {
0 => {
self.next_state = 1;
Yielded(2)
},
1 => {
self.next_state = 2;
Yielded(3)
},
2 => {
self.next_state = 3;
Yielded(5)
},
_ => Complete(())
}
}
Local variables become fields in the struct and if you have more complex control flow, the state could end up jumping around instead of just increasing by one each time. It's nothing that you couldn't write by hand, but it would be very tedious to do so.[1] https://doc.rust-lang.org/beta/unstable-book/language-featur...
Re: How Rust optimizes async/await
#104As a reminder, you don't need to use async/await to implement socket servers in Rust. You can use threads, and they scale quite well. M:N scheduling was found to be slower than 1:1 scheduling on Linux, which is why Rust doesn't use that solution. Async/await is a great feature for those who require performance beyond what threads (backed by either 1:1 or M:N implementations) can provide. One of the major reasons behi…
I find async/await easier to reason about than threads for anything more involved than the 1 request per thread web server use case. This is because you avoid bringing in the abstraction of threads (or green threads) and their communication with one another. You trade syntactical complexity (what color is your function, etc), for semantic complexity (threads, channels, thread safety, lock races).
However in multithreaded languages it's always an AND. Once you add async/await people need to know how traditional threading as well as how async/await works. Rust will also make that very explicit. Even if you use async/await on a single thread the compiler will prevent accessing data from more than one task - even it those are running on the same thread. So you need synchronization again. With maybe the upside that this could be non-thread-safe (or !Sync in Rust terms) in order to synchronize between tasks that run on the same thread. But however also with the downside that utilizing the wrong synchronization primitives (e.g. thread-blocking ones) in async code can stall also other tasks.
Overall I think using async/await in Rust is strictly more complex. But it has its advantages in terms of performance, and being able to cancel arbitrary async operations.
Re: How Rust optimizes async/await
#105Related questions: does anybody know: 1) which was the first language that introduced the async/await keywords? I want to say C#, but I’m not sure. 2) are there any papers that describe the code transformation to state machine that is commonly performed to implement these keywords?
Re: How Rust optimizes async/await
#106Earlier quoted context omitted.
Most languages allocate every future (and sub-future, and sub-sub-future) separately on the heap. This leads to some overhead, allocating and deallocating space to store our task state. In Rust, you can "inline" an entire chain of futures into a single heap allocation.
In .NET something similar is possible via ValueTask.
The introduction of ValueTask allowed C# code to only perform dynamic allocations whenever the task definitely needed to be yielded - opposed to one allocation for every Task. However it doesn't allow for guaranteed avoidance of allocations - like Rusts model can do.
However on the positive side removing the allocations for those cases is probably good enough since the other yields are of low-frequency (when waiting for IO). And Rust code currently requires allocations like Box in order to make certain things work (like type-erasure and dynamically dispatched interfaces) that are not necessarily required in .NET in the non-yielded case.
From an ergonomic perspective I definitely prefer .NETs easy-by-default model which is still highly optimizable up to the point of avoiding all allocations. But I understand why this wouldn't have worked for Rust and it's constraints (no GC, no classes, no runtime).
Re: How Rust optimizes async/await
#107I don't quite follow. What exactly is the overhead that other languages have for futures that is eliminated here?
So for example, in Kotlin each piece of synchronous code within an async function is compiled into what is essentially a Java Runnable object, which must be allocated on the heap.
Re: How Rust optimizes async/await
#108As a newcomer to Rust, wishing that this post was one of the first ones I've read about this topic. It took scouring through many many posts, some of them here on HN, to be able to grasp some of the same idea. (I may not be alone, judging from the very long discussion the other day: https://news.ycombinator.com/item?id=20719095 )
Re: How Rust optimizes async/await
#109Earlier quoted context omitted.
They have the same semantic complexity. You have tasks in async/await and you still need to deal with inter-task communication, locking, etc.
The main difference is that in async/await you control where context switches occur and the syntax (.await) explicitly points them out. This means you can often avoid locks and do things in a more straightforward manner.
Re: How Rust optimizes async/await
#110Does anyone know how Rust's implementation compares to C++2a's? The C++ people seem to have spent a lot of time creating an extremely generic framework for async/await wherein it is easy to change out how the scheduler works (I currently have a trivial work stack, but am going to be moving to something more akin to a deadline scheduler in the near future for a large codebase I am working with, which needs to be able…
Rusts design compared to that was not only focused on async functions as the main design goal, but also on maintaining compatibility with a "struct Future" type which also can be implemented by hand and represents a state-machine.
The latter will allow Rust to reuse lots of async infrastructure that had been built in the (combinators & state-machine) Futures world in the last years (e.g. the tokio library and everything on top of it).
One downside of Rusts approach might be that some parts feel a bit awkward and hard, e.g. the 2 different states of Futures (one where it hasn't been executed and can be moved around and one where it has been started executing and can't be moved anymore) and the pinning system. As far as I understand C++ exposes less of those details to end-users - this might be something where the implicit allocations might have helped it.
As far as I understand the async function flavor of C++ coroutines also have run-to-completion semantics and can't be cancelled at any yield point like Rusts Futures can be. This has the advantage of being able to wrap IO completion based operations in a more natural fashion than Rust. But it then again has the downside that users need to pass CancellationTokens around for cancellation, and that some code might not be cancellable.