Live data from Hacker News

How to think about async/await in Rust

cliffle.com

141–150 of 268 posts

Re: How to think about async/await in Rust

#141
post #4

The article shows a great example of how to implement a state machine with internal delays (do something, wait for a defined time, do something else), which is very useful in a driver or embedded context where you often just have to wait for an external device to be ready. However, it doesn't really address how you'd construct a state machine with an external tick. It's pretty common to have a state machine called at…

Your case sounds like where you'd use select()/select! to wait on multiple things? A lot of writing about async/await neglects to mention multiple potential events, but async/await's reason is really that case.

    select! {
        () = wait_for_tick() => println!("tock"),
        v = woken_thing() => println!("woke with {v}"),
    }

Re: How to think about async/await in Rust

#142
post #111

Earlier quoted context omitted.

Futures in Rust are annotated with the #[must_use] attribute [1], same as the Result type [2]. This means the compiler will emit a warning (can be upgraded to an error) if you forget to await a future even if it doesn't return anything. [1]: https://doc.rust-lang.org/nightly/src/core/future/future.rs.... [2]: https://doc.rust-lang.org/nightly/src/core/result.rs.html#49...

You don't want the safety of your program to depend on whether the compiler emits a warning or not. And turning warnings into errors just encourages people to write 'let _ = ...' to get rid of the error.

This has nothing to do with safety, just correctness.

> And turning warnings into errors just encourages people to write 'let _ = ...' to get rid of the error.

No? writing `let _ = make_future()` will clearly not await the future, why would you do it instead of just adding `.await` ?

Using `let _ = ...` is sometimes fine for Result if you really sure you don't care about the potential error you got but it's a no go with futures.

Re: How to think about async/await in Rust

#143
I'm a strong believer in structured concurrency over async.

That said, I do not know if there would be an easier way to implement the state machine in the article using structured concurrency over async. Maybe that is actually one place where async would be better.

I need to look into that.

However, for those in the comments arguing that async is better for I/O-bound stuff, I heavily disagree.

I implemented a multiplexer system. You can start any number of operations you want and then multiplex them. This blocks until one operation is done, yes, but hey, you have threads, so use another to do something else if you need.

But this multiplexer allows me to decide what function gets called for each type of task that finishes. This means that the caller still controls what to do then the "future" completes.

So it's equivalent to async, but it's still synchronous. Nice.

I can have it multiplex on multiple types of things too. I actually haven't implemented asynchronous I/O with it yet; I mostly use it to multiplex on child processes.

So I agree with one of the top-level comments: async is a hype. There are better ways for most use cases.

Re: How to think about async/await in Rust

#144

Earlier quoted context omitted.

But that's not synchronization between two concurrent things. I can still reason about queue being full in a sequential way. select! { _ = channel.readable(), if queue.has_free_space() => read(&mut queue), _ = channel.writable(), if queue.has_data() => write(&mut queue), } The point is I can implement `has_free_space` and `has_data` without thinking about concurrency / parallelism / threads. I don't need to even thin…

As I mentioned else thread, if you do not care about parallelism you can pin your threads and use SCHED_FIFO for scheduling and then you do not need any synchronization. In any case acq/rel is the only thing required here and it is extremely cheap. edit: in any case we are discussing synchronization and 'has_free_space' 'had_data' are a form of synchronization, we all agree that async and threads have different perfo…

> As I mentioned else thread, if you do not care about parallelism you can pin your threads and use SCHED_FIFO for scheduling and then you do not need any synchronization.

I don't think it is a universal solution. What if I am interested in parallelism as well, only not for the coroutines that operate on the same data? If my app handles 10k connections, I want them to be handled in parallel, as they do not share anything so making them parallel is easy. What is not easy is running stuff concurrently on shared data - that requires some form of synchronization and async/await with event loops is a very elegant solution.

You say that it can be handled with an SPSC queue and it is only one ack/rel. But then add another type of event that can happen concurrently, e.g. a user request to reconfigure the app. Or an inactivity timeout. I can trivially handle those with adding more branches to the `select!`, and my code still stays easy to follow. With threads dedicated to each type of concurrent action and trying to update state of the app directly I imagine this can get hairy pretty quickly.

Re: How to think about async/await in Rust

#145
post #14

Earlier quoted context omitted.

Asynchronous programming is a great fit for IO-driven programs, because modern IO is inherently asynchronous. This is clearly true for networking, but even for disk IO, generally commands are sent to the disks and results come back later. Another thing that’s asynchronous is user input, and that’s why JS has it. As for threading vs. explicit yielding (e.g. coroutines), I’d say it’s a matter of taste. I generally pref…

> Asynchronous programming is a great fit for IO-driven programs Yeah, but this could already be solved without "async/await compiler magic" in native code just with OS primitives, for instance with Windows-style event objects, it might look like this in pseudo-code: const event1 = read_async(...); const event2 = read_async(...); const event3 = read_async(...); wait_all(event1, event2, event3); This would run three I…

But the difference is that wait_all() is blocking the thread, right? Or does it keep running the event loop while it's waiting, so callbacks for other events can be processed on the same thread?

If it does the latter, the stack will keep growing with each nested wait call:

main -> runEventLoop -> someCallback -> wait_all -> runEventLoop -> anotherCallback -> wait_all -> ...

The async/await transformation to a state machine avoids this problem.

Re: How to think about async/await in Rust

#146

Earlier quoted context omitted.

> Any normal function call can do these things. A normal function cannot switch threads. foo(); // executed on thread 1 doSomeIO().await; bar(); // possibly continued on thread 2 Now if foo() does some native calls that write some data to thread-local storage and bar() relies on that storage - that can make a huge impact on correctness. Rust is a systems programming language, so details like that matter.

surely lifetimes and the borrow checker are a better way to statically check for these sort of issues than relying on await side effects? What if an await is inadvertently introduced later inside your (implicit) critical section?

The borrow checker does catch those issues. But it it does not do whole-program analysis. It analyzes code locally, by looking at signatures of functions being called.

And also being forced to read distant code to understand if given snippet is correct would be a maintainability nightmare.

I've had enough problems dealing with Java exceptions which are allowed to pop up from anywhere and are not visible in the code.

Re: How to think about async/await in Rust

#147
post #14

Earlier quoted context omitted.

Asynchronous programming is a great fit for IO-driven programs, because modern IO is inherently asynchronous. This is clearly true for networking, but even for disk IO, generally commands are sent to the disks and results come back later. Another thing that’s asynchronous is user input, and that’s why JS has it. As for threading vs. explicit yielding (e.g. coroutines), I’d say it’s a matter of taste. I generally pref…

> Asynchronous programming is a great fit for IO-driven programs Yeah, but this could already be solved without "async/await compiler magic" in native code just with OS primitives, for instance with Windows-style event objects, it might look like this in pseudo-code: const event1 = read_async(...); const event2 = read_async(...); const event3 = read_async(...); wait_all(event1, event2, event3); This would run three I…

> this really makes me wonder why Rust has gone down the Javascript-style async/await route with function coloring - the only reason why it remotely makes sense is that it also works in WASM

As someone who’s done asynchronous programming in Rust before Futures (I’ll call it C style), then with Futures, then with async/await, it’s because it is far simpler. On top of that it allows for an ecosystem of libraries to be built up around common implementations for problems. Without it, what you end up with is a lot of people solving common state machine problems in ways that have global context or other things going on which make the library unportable and not able to easily be reused in other contexts. With async/await, we actually have multiple runtimes in the wild, and common implementations that work across those different runtimes without any changes needed. So while I’m disappointed that we ended up with function coloring, I have to say that it’s far simpler than anything else I’ve worked with while maintaining zero overhead costs allowing it to be used in nostd contexts like Operating Systems and embedded software.

Re: How to think about async/await in Rust

#148
post #145

Earlier quoted context omitted.

> Asynchronous programming is a great fit for IO-driven programs Yeah, but this could already be solved without "async/await compiler magic" in native code just with OS primitives, for instance with Windows-style event objects, it might look like this in pseudo-code: const event1 = read_async(...); const event2 = read_async(...); const event3 = read_async(...); wait_all(event1, event2, event3); This would run three I…

But the difference is that wait_all() is blocking the thread, right? Or does it keep running the event loop while it's waiting, so callbacks for other events can be processed on the same thread? If it does the latter, the stack will keep growing with each nested wait call: main -> runEventLoop -> someCallback -> wait_all -> runEventLoop -> anotherCallback -> wait_all -> ... The async/await transformation to a state m…

Yeah it blocks the thread, any other "user work" needs to happen on a different thread. But if you just need multiple non-blocking IO operations run in parallel it's as simple as it gets.

(the operating system's thread scheduler is basically the equivalent to the JS "event loop").

Re: How to think about async/await in Rust

#149

I'm a strong believer in structured concurrency over async. That said, I do not know if there would be an easier way to implement the state machine in the article using structured concurrency over async. Maybe that is actually one place where async would be better. I need to look into that. However, for those in the comments arguing that async is better for I/O-bound stuff, I heavily disagree. I implemented a multipl…

> So it's equivalent to async, but it's still synchronous. Nice.

Based on your description this is equivalent to async/await implemented with callbacks but not async/await implemented via polling.

Re: How to think about async/await in Rust

#150

Not specific to rust, but I think asynchronous programming in general is a hype. It didn't start because it is so awesome, it started because JS can't do parallel any other way. That's the long and short of it. People wanted to use JS in the backend for some reason. The backend requires concurrency. JS cannot do concurrency. Enter the event loop. Then enter some syntactic sugar for the event loop. And since JS is pop…

Upvote from me. I couldn't have written it better myself. Async is a bug not a feature. The only problem with threading (aka CSP aka goroutines aka actors) is scalability to very large numbers of threads. imho it's better to focus on solving that problem than on switching to an unworkable alternative concurrent model.
Post reply on HN