Live data from Hacker News

How to think about async/await in Rust

cliffle.com

241–250 of 268 posts

Re: How to think about async/await in Rust

#241
post #42

Earlier quoted context omitted.

The parent comment mentions green threads. While there is some performance hit to using them, I don’t think it is way less performant. I mean, go was built for being a web backend, and is based on green threads. For rust specifically, though, green threads/coroutines were discarded because they are not zero-cost.

Until go 1.14 it basically the same as any other async/await under the hood. Every function call included an implicit .await - that is it offered a `yield` to the runtime scheduler. All the io was built around non-blocking/polling, etc. Tight loops in go would potentially screw up your app performance because there were no yields. In 1.14 they introduced some sort of preemption for tight loops too.

Stackful vs stackless, that's the big difference and the point of the original comment. Stackful abstractions are strictly more powerful of stackless ones (go coroutines subsume async/await but not viceversa).

You can have good ergonomics and performance with stackful cooperatively scheduled tasks instead of a stackless sync/await abstraction.

Async/await makes sense when you have so many tasks that you cant afford to dedicate a full stack to each of them and segmented stacks or heap allocated frames are not an option (for performance or compatibility).

Re: How to think about async/await in Rust

#242

Earlier quoted context omitted.

> Code written using threads is, at least to me, much more readable and easier to reason about. It's “easier” because it lies to you and makes you assume that everything is sequential, but it's not, and sometime that “everything is sequential” abstraction is leaky, and you can't really see what's going on without diving to the bottom of every functions. I've been accustomed so much to the transparency of async/await,…

> “everything is sequential” abstraction is leaky, Can you explain what details leak? The sequential model was developed for programming because that's a natural way to reason about proccesses. `if then else`. `do this, then do that. The "async/await" designers seem to agree, as they attempt to tame async by emulating this behavior. Note that to do anything other than sequential is extremely complicated to reason abo…

> Can you explain what details leak?

You answer half of it a few lines later:

> All sorts of concerns like: race conditions, synchronization, dead lock, etc are inherent. > Any approach that does not directly address these issues is the one that's creating a leaky abstraction.

By writing `await` you're telling your reviewers, coworkers and even your future self than your program stops executing sequentially at this step, and that other concurrent task can do things in the meantime. When using blocking code, the same thing can happen, but this is hidden from you.

But in my perspective as a back-end engineer, the biggest issue is related to latency: with annotations you know (and tell others: code is written once but read many time) what takes significant time, with threads and hidden yield point you don't. It looks sequential, but the latency is an observable behiavor that show it's not: the definition of a leaky abstraction.

> All functions take "blocking time" to execute. It's a spectrum of how long you want to wait

That's technically correct, but keep in mind that the magnitude difference between your typical REST API call and a CPU instruction is roughly the same as the difference between the size of a football field and the distance to the Sun…

Re: How to think about async/await in Rust

#243

Concurrency is like a single cook preparing many different recipes at the same time, and parallelism is multiple cooks in the same kitchen. Single-threaded processes can use async/await to context switch and it feels like parallelism but it's not unless you're executing each task to its own OS thread. ... and that didn't click for me until I understood that concurrency (async/await) and parallelism (multiple OS threa…

Yeah, async is a type of greenthreading.

When I think of green threads / fibers, its threading handled in user space. Where a stack is starts small and it can grow and its managed by the language’s runtime.

Re: How to think about async/await in Rust

#244
post #155

Earlier quoted context omitted.

But concurrency is just "single core parallelism" anyways, so this isn't really germane to the discussion. JS has neither.

Concurrency is not "single core parallelism". Concurrency describes tasks/threads of execution making independent progress of each other. Parallelism describes tasks/threads actually running at the same time.

>Concurrency is not "single core parallelism"

Of course it is. Concurrency gives the impression to the user that parallel processing is being done, even when it's not. That's why my parents old 386 could render a moving mouse cursor and a progress bar at the same time (usually).

Concurrency lets you do things "in parallel" even if you can't actually do them in parallel.

Re: How to think about async/await in Rust

#245

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

There is an implicit mutex/barrier/synchronization in the join.

Re: How to think about async/await in Rust

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

There are barriers for join. But without barriers, the risk is compiler reordering/lift to registers/thread scheduling. The CPU cache would not be the direct cause of any “stale” reads. https://news.ycombinator.com/item?id=36333034

Re: How to think about async/await in Rust

#247

Earlier quoted context omitted.

> The fact that Microsoft had a hand in async/await’s emergence may influence how people feel about it. I can see this.

This whole thread reads like backlash to the async hype, not so much the merits of the paradigm itself.

Yep

Re: How to think about async/await in Rust

#248
I like this quote. Forgot where I first heard it.

    Threads are for working in parallel, async is for waiting in parallel.
If your app is doing the work, use threads. If something else is doing the work like a database engine or the kernel retrieving a file from disk, use async since your app is just waiting.

Re: How to think about async/await in Rust

#249

Earlier quoted context omitted.

> When waiting for an i/o bound operation, a thread can simply sleep. I mean if you're fine with blocking I/O then obviously you don't need async, but on the other hand having non-blocking I/O is the whole point of async ^^

It really just depends on what you mean by non-blocking I/O. Most node code I see in the wild is just a simple `await loadData()` which doesn't block the main node thread but does block that code flow until the data returns. This is roughly the same as what would happen in a normal blocking multithreaded language other than the extra overhead of a thread. If you don't have enough threads (or they are efficent enough…

> It really just depends on what you mean by non-blocking I/O.

> Most node code I see in the wild is just a simple `await loadData()` which doesn't block the main node thread but does block that code flow until the data returns.

Agreed. Higher level languages tend to discourage or outright decide not to expose asynchronous I/O. Instead, they optimize blocking I/O within their own runtime - skipping the higher resource needs for the system schedule and thread representation.

If I am writing a web server in C or C++, I'm likely writing asynchronous I/O directly. I may also decide to use custom memory strategies, such as pooling allocators.

If I write one in classic Java, I'm allocating two threads to represent input and output for each active connection, and hoping the JVM knows how to make that mass of threads efficient. In Go, I'm likely using a lot of goroutines and again hoping the language runtime/standard library figured out how to make that efficient.

Java packages like NIO/Netty and Go packages like gaio are what expose asynchronous programming to the developer.

The footgun is that it is hard to use an asynchronous I/O package when you have a deep dependency tree that may contain blocking code, perhaps in some third party package. This was one of the attractions to server-side javascript; other than a few local filesystem operations, everything sticks to the same concurrency model (even if they may interact with it as callbacks, promises or async/await)

Re: How to think about async/await in Rust

#250

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…

Proper asynchronous programming is not something that's hype. In fact, it's something that is quickly disappearing from most I/O-bound code. Low-level asynchronous I/O (think epoll, kqueue, IOCP) was popularly implemented by many high-performance servers in the early 2000s, often as response to the C10k problems. It's really Nginx, Haproxy, Lighttpd, libevent (and later libev and libuv) which popularized this programming style in systems programming.

It's worth noting that during the 1990s, multi-threaded programming did not completely dominate as the model for network servers. I believe it was mostly due to multi-threading support was uneven across the different UNIX flavours of the day, but regardless of the cause, some popular servers (mostly notably Apache httpd) started out as multi-process based, using a pool forks the same way you'd use a thread pool. Other servers were written using the 1990s incarnation of asynchronous programming, essentially using select() or poll() (or WSAWaitForMultipleEvents on windows). From the programmer's perspective, these act mostly the same epoll, but are just less efficient.

It is during that time that the C10k problem and its asynchronous solution was experiencing its peak hype cycle that high-level languages got interested in the game, and implemented asynchronous I/O with callbacks. I believe it started with Python and Twisted, but node was the poster-child. OS-level threads were either not supported by the language (Node.js) or severely encumbered by having a GIL (Python). Green threads or coroutines would have probably been a better fit for this languages, but if you're just writing a library or a runtime for a language you don't control, that's harder (of course, gevent in Python went and manage to do that anyway).

By the time async/await came to Javascript, this wasn't part of a hype. Javascript has already widely adopted callbacks and then promises as a bottom-up, library oriented solution. Most I/O APIs were promise-based. Even if ES6 added go-like coroutines, all the APIs you had were already accepting a callback or returning a promise. You'd still had to do something like "await(myApi())" every time you're calling that API, not to mention having to introduce synchronization primitives to the language and watching code that never had to care about synchronization before break.

Async/await by itself, is not really asynchronous programming. Behind the scenes, it is implemented asynchronously (just the same as I/O in goroutines is!), but the programmer is writing code that looks linear and synchronous. The real trend nowadays is to eschew synchronous I/O and hide the complexity of asynchronous I/O behind synchronous-looking code. Explicitly asynchronous programming (like callbacks or non-awaitable promises) is just as trendy as Ruby on Rails or flip phones, that is - yeah, sure, it was fairly trendy back in the 2005.

Nowadays you've got two popular M:N thread models for running multiple synchronous tasks which perform asynchronous I/O behind the scenes: The green thread model (Go, Java's Virtual Threads) and the state machine transformation model (a.k.a. async/await). If you think the async/await model is inferior to the green thread model used by Go, that's a different story. I think each has its own pros and cons, but claiming that only async/await receives hype is untrue. The green threading model receives a fair share of its own hype ("Which color is your function"), and its usually the proponents of the green threading model who claim that their model is strictly superior while the other model has no merit at all, and not otherwise.

If you go back to Rust, Rust definitely tried the green threads model, as many people have already said. It had to abandon it. Go is not to be a full-spectrum systems language, and can get along pretty well with being garbage collected and running its own scheduler. Rust has to run on some environments and contexts where you just can't do that. Rust is also very sensitive to overhead introduced by features (that's the entire "zero-cost abstraction" theme), and it does not shy away from adding some complexity in exchange of performance. Otherwise why won't it just do away with lifetimes altogether?

While I concur the callbacks hype in JS was probably misguided (although understandable), I find it hard to believe that the decision to use async/await in Rust was based on hype.

Post reply on HN