Live data from Hacker News

How to think about async/await in Rust

cliffle.com

101–110 of 268 posts

Re: How to think about async/await in Rust

#101

Earlier quoted context omitted.

> We still need them if we have multiple coroutines using a shared resource across multiple yield points. We still need them if we have multiple parallel tasks (coroutines spawned non-locally) using a shared resource across multiple yield points. As long as the accesses to the shared variable are separated in time, sharing is fine. This is correct code: let mut foo = 1; async { foo += 1 }.await; foo += 1; println!("{…

the equivalent threaded code wouldn't need a mutex either: int foo = 1; std::thread ([&] { foo+=1; }).join(); foo+=1; std::cout (sorry for the C++, I don't speak much rust).

You definitely need a mutex here (or use atomics), otherwise you have a race condition

Re: How to think about async/await in Rust

#102

Thanks for this article. I feel the goal of a tool should be to make common patterns easy to represent: so "sprinkling async everywhere" only doesn't work because of some common desirable pattern not being easily representable in modern languages and gotchas of the languages. I have a lightweight thread scheduler written in C and I communicate between threads with a lockless ringbuffer. IO threads do IO. I like the i…

Maybe take a look at pi-calculus and session types if you're not familiar, the notation you're describing sounds very similar/related to that

Thanks for your reply.

I am coming from the perspective of states and behaviour. I have read about session types but my syntax is not inspired by them.

Types are important and useful but I am more interested in the rigid parts of code that types data flow through otherwise known as control flow. I feel it's an ignored part of computer science.

My goal is easy parallelism, asynchronocity and reactivity.

Re: How to think about async/await in Rust

#103
post #31

Earlier quoted context omitted.

It would just make things more explicit. Whenever you want to obtain a future you'd have to add "async". The execution of async stuff would work the same just instead of having to explicitly "await" things you'd have to explicitly "async" things. Of course you can't change the way Rust does async/await now without having to rewrite all the async code so not going to happen.

I don't think so, it would only mislead and hide what's really going on. Creating a future in Rust does not have any side effects like running the future in background. This is not JS. Creating a future is just creating an object representing future (postponed) computation. There is nothing spawned on the executor. There are no special side effects (unless you code them explicitly). It works exactly as any other func…

    > Creating a future in Rust does not have any side effects like running the future in background. This is not JS. Creating a future is just creating an object representing future (postponed) computation. There is nothing spawned on the executor. There are no special side effects (unless you code them explicitly). It works exactly as any other function returning a value, hence why should it be syntactically different?
Fair point.

    > Contrary, an `await` is an effectful operation. It can potentialy do a lot - block execution for arbitrary long time, switch threads, do actual computation or I/O... So I really don't understand why you want to hide this one.
I disagree here. Any normal function call can do these things. On the other hands an async function returning a future does nearly nothing. It sets up an execution context but doesn't execute (in Rust). But they usually look like a function call that actually performs the action - not so! An explicit "async" in front of it would make the program flow more clear instead of hiding it.

    > Maybe the naming is confusing - because `await` does not really just `await`. It runs the future till completion. You should think about it more as if it was named `run_until_complete` (although it is still not precise, as some part of that "running" might involve waiting). 
That's exactly speaking to my previous point. The program flow is not 100% immediately obvious anymore. One could argue that "await" is fine as is but maybe adding "async" to the call and not just function signature would add clarity.

Re: How to think about async/await in Rust

#104

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…

Async is, in many situations, better than traditional threads. Threads are a resource hog. They take a lot of system resources, and so you usually want to have as few of them as possible. This is a problem for applications that could, in theory, support thousands of concurrent connections, if not more. With a basic thread-based model, you need 1 thread per connection, and if you have long-lived connections with infre…

> Threads are a resource hog.

I agree. They are if we spawn OS threads everytime we need a thread. The equivalent in async would be spawning the entire overhead of the event loop every time we need concurrency.

Obviously, we don't do that.

WorkerPools don't need respawning. Greenlets don't need respawning. Virtual Threads handled by the runtime don't need respawning.

Re: How to think about async/await in Rust

#105

Earlier quoted context omitted.

> We still need them if we have multiple coroutines using a shared resource across multiple yield points. We still need them if we have multiple parallel tasks (coroutines spawned non-locally) using a shared resource across multiple yield points. As long as the accesses to the shared variable are separated in time, sharing is fine. This is correct code: let mut foo = 1; async { foo += 1 }.await; foo += 1; println!("{…

the equivalent threaded code wouldn't need a mutex either: int foo = 1; std::thread ([&] { foo+=1; }).join(); foo+=1; std::cout (sorry for the C++, I don't speak much rust).

Don't you need some kind of way of telling the compiler you would like barriers here? I think otherwise the helper thread could run on another cpu and the two cpus would operate on their own cached copies of foo. But then again I'm not 100% on how that works.

Re: How to think about async/await in Rust

#106
post #92

Earlier quoted context omitted.

> Only if you know that foo is an async function. You can't tell by the function call itelf. That's fair point, but traditionally you don't use blocking functions in async contexts at all. It is fairly easy to lint for by prohibiting some inherently blocking calls eg.g std::io, although they might sneak in through some third-party dependency. This doesn't have an easy solution because Rust is a general purpose langua…

You can't change the async/await rules of Rust anymore. I get that. But if it started like I described from the beginning I don't see why that wouldn't work. It's just a question of syntax. Someone adding a blocking call 5 layers down wouldn't be any different than someone adding an "await foo()" right now. Code would still compile fine. As long as everything follows the same rules. Can't mix them obviously.

> wouldn't be any different than someone adding an "await foo()" right now

It would. `.await` works only inside `async` context. So if the method wasn't async at the top level, then adding `.await` somewhere down the call chain would force changing all the signatures up to now become `async`.

So you cannot just freely add `.await` at random places that don't expect it. Which is sometimes a blessing and sometimes a curse. Definitely when trying to hack a quick and dirty prototype this is a slowdown. But it is really good when you aim for low latency and predictability.

Re: How to think about async/await in Rust

#107

Earlier quoted context omitted.

the equivalent threaded code wouldn't need a mutex either: int foo = 1; std::thread ([&] { foo+=1; }).join(); foo+=1; std::cout (sorry for the C++, I don't speak much rust).

You definitely need a mutex here (or use atomics), otherwise you have a race condition

Where exactly? Can you point me to the data race? Consider that the thread constructor call happens-before the thread start and the thread termination happens-before the join call returns.

Re: How to think about async/await in Rust

#108
post #103

Earlier quoted context omitted.

I don't think so, it would only mislead and hide what's really going on. Creating a future in Rust does not have any side effects like running the future in background. This is not JS. Creating a future is just creating an object representing future (postponed) computation. There is nothing spawned on the executor. There are no special side effects (unless you code them explicitly). It works exactly as any other func…

> Creating a future in Rust does not have any side effects like running the future in background. This is not JS. Creating a future is just creating an object representing future (postponed) computation. There is nothing spawned on the executor. There are no special side effects (unless you code them explicitly). It works exactly as any other function returning a value, hence why should it be syntactically different?…

> 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.

Re: How to think about async/await in Rust

#109

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…

async/await allows to do concurrency without the need for explicit synchronization to shared data structures. E.g. I can do: loop { select! { _ = src_channel.readable() => src_channel.read(&mut buffer), _ = dst_channel.writable() => dst_channel.write(&mut buffer), } } without any mutex guarding the buffer, even though the reads and writes happen concurrently and share the same mutable buffer. This is possible because…

> Which obviously would be much harder to get right.

Not obvious to me I'm afraid. Using CSP, this is almost trivially easy. All access to the data goes through a guardian thread. Accessing the resource is just sending a message.

And mutexes are not hard either.

Re: How to think about async/await in Rust

#110
post #105

Earlier quoted context omitted.

the equivalent threaded code wouldn't need a mutex either: int foo = 1; std::thread ([&] { foo+=1; }).join(); foo+=1; std::cout (sorry for the C++, I don't speak much rust).

Don't you need some kind of way of telling the compiler you would like barriers here? I think otherwise the helper thread could run on another cpu and the two cpus would operate on their own cached copies of foo. But then again I'm not 100% on how that works.

No. All synchronization edges are implied by the thread creation and join. Same as for the async/await example.
Post reply on HN