Live data from Hacker News

What async promised and what it delivered

causality.blog

241–250 of 317 posts

Re: What async promised and what it delivered

#241

Earlier quoted context omitted.

The amount of stack you pay for on a thread is proportional to the maximum depth that the stack ever reached on the thread. Operating systems can grow the amount of real memory allocated to a thread, but never shrink it. It’s a programming model that has some really risky drawbacks.

> Operating systems can grow the amount of real memory allocated to a thread, but never shrink it. Operating systems can shrink the memory usage of a stack. madvise(page, size, MADV_DONTNEED); Leaves the memory mapping intact but the kernel frees underlying resources. Subsequent accesses get either new zero pages or the original file's pages. Linux also supports mremap, which is essentially a kernel version of reallo…

> C programs can't do it because pointers to stack allocated objects may exist.

They sure shouldn't exist to the unused region of the stack though; if they do, that's a bug (because anything could claim that memory now). You should be free and clear to release stack pages past your current stack pointer.

Re: What async promised and what it delivered

#242
post #174

Earlier quoted context omitted.

There's much ridiculous hatred for OS threads based on people's biases of operating systems and hardware from 20 years ago. So much so that they'll sign themselves up for async frameworks that thread steal at will and bounce things all over cores causing cache line bouncing and associated memory stalls, not understanding what this is doing to their performance profile. And endure complexity, etc. through awkward asyn…

hardware from 10 years ago - do you have benchmarks for more recent hardware? https://vorner.github.io/async-bench.html

I don't think those benches are much of a flex, even by the author's own description you'd be fine with any of them. They all have acceptable performance and don't show any order of magnitude differences or non-linear scaling problems.

Further, the benches that are showing best there are non-thread-stealing scenarios, not tokio.

I also suspect simply tuning the thread-based workloads more aggressively would have the same effect.

When I profile high throughput tokio applications there's way too much contention on shared atomics, mostly inside tokio's scheduler itself. On lower core count machines and where the workload is I/O heavy, this is probably fine. So, yes, web servers.

But I'm very interested in applications that scale on machines with lots of cores and where CPU is a large part of the equation.

Re: What async promised and what it delivered

#243

Earlier quoted context omitted.

IIRC ZIO solution is actually to return a generic E :> X|Y. Caller providing the callback knows what else is on E, and they're the only one that knows it so only they could've handled it anyway. You still get type inference. Or if you mean that returning a new error breaks API compatibility, then yes that's the point. If now you can error in a different way, your users now need to handle that. But if it's all generic…

If you declare specific error types and callers only write handlers for specific cases, then adding a new error breaks them. If you just declare a base error type in your API, they have to write a generic error handler or it doesn't type check. In this way, declaring a type guides people to write calling code that doesn't break, provided you set it up that way. It makes things easier for the implementation to change.…

Usually in Scala errors subtype Throwable, so as long as new union members continue to do so, if you wanted a generic handler (e.g. you just log and return) you could handle that. But you can also be more specific with actual logic, with the benefit that if you choose to do so and the underlying implementation changes, you detect it.

Go also basically requires you to write actually generic error code (if err != nil return err), so it feels like errors are more work than they are. In Scala (especially ZIO) propagation is all automatic and you just handle it wherever you'd like. The only code that cares about errors is the part generating them and the part handling them. But it's all tracked in the type system so you can always see what exact errors are possible from what methods. And it's all simple return values so no trickiness with exceptions (e.g. across async boundaries).

Re: What async promised and what it delivered

#244

No mention of JVM.. which is a bit odd as recently is kinda solved this problem. Sure, not all use cases, but a lot. It uses N:M threading model - where N virtual threads are mapped to M system threads and its all hidden away from you. All the other languages just leak their abstractions to you, java quietly doesn't. Sure, java is kinda ugly language, you can use a different JVM language, all good. Don't get me wrong…

Solved assuming you can afford a huge embedded-unfriendly runtime.

Re: What async promised and what it delivered

#245
post #203
post #87

> Language designers who studied the async/await experience in other ecosystems concluded that the costs of function coloring outweigh the benefits and chose different paths. Not really. The author provides Go as evidence, but Go's CSP-based approach far predates the popularity of async/await. Meanwhile, Zig's approach still has function coloring, it's just that one color is "I/O function" and the other is "non-I/O f…

That is an unfair characterization of Zig. The OP correctly points out: > Function signatures don’t change based on how they’re scheduled, and async/await become library functions rather than language keywords. The functions have the same calling conventions regardless of IO implementation. Functions return data and not promises, callbacks, or futures. Dependency injection is not function coloring.

> The functions have the same calling conventions regardless of IO implementation.

Okay, then by that definition, Rust doesn't have colored functions either, because `async fn foo() -> Bar {` in Rust is just syntax sugar for `fn foo() -> impl Future {` (that's a normal function that returns a future), and `Future` is just a normal trait that provides a `poll` method; there's no different calling convention anywhere. So which is it? Either Rust and Zig both have colored functions, or neither do.

Re: What async promised and what it delivered

#246
post #62

I will agree - async rust on an operating system isn’t all that impressive - it’s a lot easier to just have well defined tasks and manually spawn threads to do the work. However, in embedded rust async functions are amazing! Combine it with a scheduler like rtic or embassy, and now hardware abstractions are completely taken care of. Serial port? Just two layers of abstraction and you have a DMA system that shoves byt…

you’re talking to the core of the issue. In no other language did they try to satisfy the case of running on an embedded system vs general purpose computing. Async rust tried to, and came up with a solution that is not great for the majority of programmers writing rust.

I wish to God that the rust library devs would admit to this fact - say that async rust should stay for embedded runtimes usecases, but we shouldn’t be forcing async across the majority of general purpose computing libraries. It’s just not a pleasant experience to write nor read. And it really doesn’t give any performance benefits.

Re: What async promised and what it delivered

#247
post #238
post #202

Earlier quoted context omitted.

They are not perfectly fine. If a task panics then you will get the right stack trace, but there is no way to get a stack trace for a task that’s currently waiting. (At least not without intrusive hacks.)

Would this be considered an intrusive hack? https://docs.rs/tokio/latest/%20tokio/runtime/struct.Handle....

> This functionality is experimental, and comes with a number of requirements and limitations.

I assume that answers your question.

Re: What async promised and what it delivered

#248
post #228

Earlier quoted context omitted.

I have some test code that runs a comparison of Hyper pre-async (aka thread per request) vs async (via Tokio), and the pre-async version is able to process more requests per second in every scenario (I/o, CPU complex tasks, shared memory). I'll publish my results shortly. I did these as baselines because I'm testing finishing the User Managed Concurrency Groups proposal to the linux kernel which is an extension to pr…

Relevant prior work: https://github.com/jimblandy/context-switch

Thank you for this! This is really helpful.

The UMCG implementation allows kernel thread context switches to happen in 150-200 microseconds, compared to the 1500-2000 microseconds for normal kernel thread context switches. My goal is to show that if UMCG could be merged into the Linux run time then then it would be competitive with async rust without the headache.

Re: What async promised and what it delivered

#249
post #238

Earlier quoted context omitted.

Would this be considered an intrusive hack? https://docs.rs/tokio/latest/%20tokio/runtime/struct.Handle....

> This functionality is experimental, and comes with a number of requirements and limitations. I assume that answers your question.

So once it's out of the experimental stage it won't be an intrusive hack anymore?

Re: What async promised and what it delivered

#250
post #62

I will agree - async rust on an operating system isn’t all that impressive - it’s a lot easier to just have well defined tasks and manually spawn threads to do the work. However, in embedded rust async functions are amazing! Combine it with a scheduler like rtic or embassy, and now hardware abstractions are completely taken care of. Serial port? Just two layers of abstraction and you have a DMA system that shoves byt…

you’re talking to the core of the issue. In no other language did they try to satisfy the case of running on an embedded system vs general purpose computing. Async rust tried to, and came up with a solution that is not great for the majority of programmers writing rust. I wish to God that the rust library devs would admit to this fact - say that async rust should stay for embedded runtimes usecases, but we shouldn’t…

I write reams of async rust for a living, and completely disagree with this characterization. The concurrency primitives in the futures crate are able to elegantly model the large majority of places where concurrency is needed, and they are nicely compostable.

More than once, we have wanted to improve the performance of some path and been able to lift the sequential model into a stream, evaluated concurrently with some max buffer size. From there, converting to true parallel execution is just a matter of wrapping the looped futures in Tasks.

Obviously just sprinkling an async on it isn’t going to make anything faster (it just converts your function into a state-machine generator that then needs to be driven to completion). But being able to easily and progressively move code from sequential to concurrent to parallel execution makes for significant performance gains.

Post reply on HN