Live data from Hacker News

The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

pmbanugo.me

21–30 of 63 posts

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#21
post #14

I am not really sure how much yet another post complaining about async/await that ends with “thread-per-core is the way to go” adds to this discussion. Granted I’m both an Erlang programmer and a big fan of Tokio and Rust’s async/await implementation in general and I think this post and many others like it betray a fundamental misunderstanding of these technologies so I am probably biased.

When I learned about async/await when it came out with .NET, they put tremendous amount into explaining that async/await is not concurrency. But that was in time when you did a training when a new version of your programming stack came out and you did not consume knowledge in 30s snippets.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#22

A simple burst of memmap + soft fault with 100 or 1000 threads on a normal laptop would tell you that thread contention is real and cache locality gets destroyed. Couple that with pinned threads. You can see the latency increase by increasing thread count. Add to that the motherboard interconnect tax for numa systems. Work stealing is not the way for increasingly many workloads on modern hardware.

Recently we built Dip, our in-house ephemeral + parallel database, and we went with may coroutines + work pinning to the same thread which also nicely becomes numa aware via architecture.

Increasing threads beyond system's hardware cores/threads resulted only in marginal gains of a couple of milliseconds worth of differences on huge workload with large increase in memory (thread stack) used by the massive number of threads.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#23
I was looking forward to checking out Project Tina until I realised it was a completely different language. Classic story. Surely you can build a thread-per-core message passing concurrency framework in Rust, the language is designed to allow such alternatives.

Is Project Tina a bit like the Actor Model, but having actors pinned to cores?

And I don't understand how Tina deals better with the problem of compute-heavy tasks blocking the thread. It looks to me like it is also cooperative concurrency per core, and if one Isolate runs for a long time the other Isolates in that core will not be able to handle their messages.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#24
This is nonsense. Tokio was built for I/O, not crunching numbers. Most programmers know to use spawn_blocking to crunch 10MB JSON, if needed.

Additionally, the proposed workload per thread model will be orders of magnitude slower than Tokio for I/O-bound workloads, which most applications are.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#25
post #7

I think it's important to understand how we got here and a lot of it has to do with serving network requests or RPCs. The first Web servers used CGI (Common Gateway Interface). This spawned an entire program (process) per request and had obvious overheads. This led to some optimizations (eg FastCGI, ISAPI/NSAPI) to reduce the overhead. This was the era of Perl scripts being popular. Then came the model of having a pe…

That's an informative overview. Somewhere in the story was Node.js and libuv, the callback style, promises, and the popularization of the async/await paradigm. Not sure if there was a direct influence on Rust's async libraries, but I imagine it affected how some people think what an intuitive async syntax might look like.

Particularly node.js single threaded operational model basically eradicating concurrency from the brains of a generation of developers.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#26
post #13

Earlier quoted context omitted.

To be fair, the Rust async model itself was intentionally designed not to be prescriptive in the way you describe. You can build, and there exists, different task executors that can handle things like priority and many other execution models. Async is just a way to describe a tree of concurrent tasks that may depend on (wait on) each other at certain points. It is mostly declarative. Tokio has taken over as the defau…

Outside of Embassy in embedded, tokio is the only realistic choice though, because it is likely that any third party async crate has a dependency on it already. Yes, smol, monio, glommio etc exist, but they are marginalised (and as far as I can tell they don't really help that much with mixed IO / compute workloads). In fact, async/await in Rust falls apart with a mixed IO / compute workload since scheduling is coope…

Yes I do agree. It's not that Tokio has taken over just due to community momentum. There's a certain subtle lock-in that happens due to how the Rust type system and dependency system work together. It's a hard problem to address.

Regarding mixed IO/compute and preemptive scheduling, well that's what threads are. They are not as lightweight, but they are there and they are quite ergonomic to use in Rust.

I could even imagine an async executor that simply starts a thread per task, so that you can keep the nice syntax, but that's obviously not ideal from a performance standpoint. Tokio already dispatches tasks to a thread pool, it is not completely cooperative, it's the sensible way to do this. And there's always tokio::spawn_blocking too.

PS: Actually, it is underrated how well designed classic threading is in Rust. It was just going out of vogue when they did it, but they addressed so much of the complexity that was around for years in Java, C++ and the like. They did actually mostly achieve the holy grail of fearless concurrency and with a more ergonomic design.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#27
This reads an awful lot like the prompter had a semi-confusing time with some async, and had their favourite model write an upset blog post about it.

I don't think these systems are perfect, nor are they fit for every use-case, but some of the complaints ring a bit hollow.

> fetching a database record over the network, then immediately crunching the data. But what happens when that data crunching involves parsing a 10MB JSON payload

Mixing IO-sensitive code and blocking code causes issues, who knew? I'm not quite sure how these libraries are supposed to magically _save_ you from this?

> When these latency spikes occur, the answer is always the same: separate your runtimes.

Well yeah. "Why doesn't my daily-driver cut sick lap times around the Nurburgring?" If you want to run blocking, non-interleaved code, don't do it in an executor expecting small, interleaved, non-blocking tasks. The docs for Tokio even mention this, and provide a number of worked examples of integrating/bridging sync and async code.

> If a developer must manually partition I/O and compute, strictly police the boundaries to prevent deadlocks, and ferry data between two different runtimes with two different mental models, the async abstraction has failed.

Not necessarily. If I'm chasing a performance target, and the tool gets me 80-90% of the way there, to the point where my next task is optimising layout and caching, I call that a win. That's performance ground we'd have to address at some point if we want to go faster, so getting there easily means: - those people who don't need to go faster because their perf requirements are met are happy - those people who need to go further get to skip the intervening work, and go straight to these optimisations.

Edit: also wanted to make a point about the "memory blowup" the author mentions. It's entirely possible to do the _same_ load-shedding the author wants in Rust/Tokio/Monoio/etc, this is again, just a matter of building your application in a way that uses, and communicates back-pressure. I gather that they appear to want something which is a bit more opinionated and "batteries included" as far as functionality goes, which is totally fine-and-cool, but calling Tokio + Rayon _bad_ because they explicitly don't do this is a bit of a miss.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#28

I see two solid points here: 1. It's not reasonable to expect the application layer to carefully partition its work into "I/O heavy" and "CPU heavy" parts. 2. It's not reasonable to queue up an arbitrary amount of work without back-pressure. I haven't used Tokio much, but if it falls prey to these pitfalls, it would make me pause before adopting it. I think there are probably ways of using Rust async that don't fall…

> but if it falls prey to these pitfalls, it would make me pause before adopting it.

This issue isn't really a Tokio concern, it's pretty straightforward to write Rust code that has back pressure mechanisms. The "it's not reasonable" in my mind implies that if someone goes and _does that_ then there's not much a library can do to restrain the developer.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#29
post #23

I was looking forward to checking out Project Tina until I realised it was a completely different language. Classic story. Surely you can build a thread-per-core message passing concurrency framework in Rust, the language is designed to allow such alternatives. Is Project Tina a bit like the Actor Model, but having actors pinned to cores? And I don't understand how Tina deals better with the problem of compute-heavy…

I'm not 100% sure, but it looks like Tina forces your IO to be return values. You can't actually do IO inside the Isolate handlers, so your compute code runs and returns the next IO operation to run. That's what it meant by you need to be explicit about the state machine, you have to have a handler for before I make this IO, and a handler for after that IO has run.

Isolates are like synchronous state machines. During each handler invocation, an isolate processes one message, mutates its private state, and chooses its next scheduler action. If IO is needed, it returns an IO Effect describing the IO, the isolate is parked, and the eventual IO result is delivered through a later completion message. In the meantime it continues to handle messages of other isolates.

Edit: And I realized you asked about compute-heavy tasks, nevermind, it does not seem to solve that.

Re: The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

#30

A simple burst of memmap + soft fault with 100 or 1000 threads on a normal laptop would tell you that thread contention is real and cache locality gets destroyed. Couple that with pinned threads. You can see the latency increase by increasing thread count. Add to that the motherboard interconnect tax for numa systems. Work stealing is not the way for increasingly many workloads on modern hardware. Recently we built D…

> Increasing threads beyond system's hardware cores/threads resulted only in marginal gains of a couple of milliseconds worth of differences on huge workload with large increase in memory

Careful, if you say that too loudly, the "get rid of async just spawn more threads!!!!!" people will come out of the woodwork to yell at you about how _all_ async is a lie and we should instead pretend none of it exists and just spawn more threads.

Post reply on HN