Live data from Hacker News

Local async executors and why they should be the default

maciej.codes

71–80 of 327 posts

Re: Local async executors and why they should be the default

#71

Suggesting that single threaded concurrency is the right way to do it when building tooling is completely asinine.

He isn't suggesting that. He's suggesting it is the right place to start , in the same way that we normally start writing sync code with a single thread.

Nope. Direct quote:

> “..and when you need to utilize multiple CPU cores, you just spawn multiple processes that listen on the same socket. This is a much better way of structuring servers..”

Re: Local async executors and why they should be the default

#72
post #21

Earlier quoted context omitted.

Multi threading and concurrency are not the same. You can have a very high performance server that handles thousands of requests concurrently on a single thread. That's how node/deno do things. But the way to do things in async rust is that if you want concurrency you also have to use multithreading. At least that is what you see in all the examples and docs. As soon as you require your futures to be Send you have to…

> So e.g. you have to use Arc > In terms of C++ code that would equate to std::shared_ptr > which ... sounds quite wasteful in terms of scalability/performance. Why is it not possible to simply return a Rust promise? That's the way I do it in my C++ async (executor) library backed by work-stealing queues under the hood.

I think it's worth understanding why you need Arc and RwLock here.

If the compiler fails to type check because a future is not Send, it means that the future's context holds onto some state that is not safe to be sent to a different thread. For example, holding a reference to something on the stack across an await point. If it fails because something is not Sync, it means the future's context holds onto some state that is potentially accessed concurrently from multiple threads.

If you write your async code such that it does not have these concurrency bugs then you don't have a problem. Arc is a convenient way to make something that is Send and RwLock is a convenient way to make something Sync. You can also fix your code by not introducing concurrency bugs.

This isn't perfect though because the errors are often cryptic. You can have a function that is fine in one context completely break when called in another with "future is not send". And good luck identifying where the bug is and how to fix it.

As for constructing a future directly, sure you can do that. But keep in mind Rust futures aren't promises that just wrap some .then callback. They are types that implement the Future trait which has a method .poll that takes self by Pin and a Waker object to prevent the underlying data from being moved and invalidating references as well as wake other pending futures. If that sounds complex, it is, because async in Rust is actually super complicated and the syntactic sugar does a massive amount of work.

Re: Local async executors and why they should be the default

#73
post #17

> Yes the RwLock and mpsc comes from Tokio and lets you .await instead of blocking a thread, but these are not async primitives, these are multi-threading synchronization primitives. The only reason all this async stuff even exists is because we want concurrency. We want to say "while this one task waits for I/O, this other task will do stuff". So it's not too surprising to me that an intro to async would include syn…

Async/await is green threads with better scoping and syntactic sugar. Which lets be honest, is something green threads desperately needed.

They're not green threads. Futures are stackless coroutines.

Re: Local async executors and why they should be the default

#74
post #45

> Making things thread safe for runtime-agnostic utilities like WebSocket is yet another price we pay for making everything multi-threaded by default. The standard way of doing what I'm doing in my code above would be to spawn one of the loops on a separate background task, which could land on a separate thread, meaning we must do all that synchronization to manage reading and writing to a socket from different threa…

> Also, IMO it's relatively easy to use Send-bounded future in non-Send(i.o.w. single-threaded) runtime environment, but it's almost impossible to do opposite. Ecosystem users can freely use single threaded async runtime, but ecosystem providers should not.

We have Send and non-Send primitives in Rust for a reason. You could use Arc/Mutex/AtomicUsize/... everywhere on a single thread, but you should use Rc/RefCell/Cell/... instead whenever possible since those are just cheaper. The problem is that in the ecosystem we are building the prevailing assumption is that anything async must also be Send, which means we end up using Send primitives even in non-Send contexts, which is always a waste.

> If you want every users to only use single threaded runtime, it's a major loss for the Rust ecosystem.

Running single threaded executors does not prohibit you from using threads, it just depends on how you want to do that. You can:

1. Have a single async executor running a threadpool that requires _everything_ to be Send. 2. Have a single threadpool, each thread running its own async executor, in which case only stuff that crosses thread boundaries needs to be Send.

The argument is that there are many scenarios where 2 is the optimal solution, both for performance and developer experience, but the ecosystem does not support it well.

Re: Local async executors and why they should be the default

#75

"If you write regular synchronous Rust code, unless you have a really good reason, you don't just start with a thread-pool. You write single-threaded code until you find a place where threads can help you, and then you parallelize it, [..]" I cannot agree more with that. As someone who's done a good deal of Java in my day job, I can tell you a thing or two about spawning threads willy-nilly. At least it is easier to…

It is opt-in. If you're using Tokio then you can specify whether you want to use a single-threaded or multi-threaded runtime. Multi-threaded is "default" in the sense that if you just use `#[tokio::main]` then you get a multi-threaded runtime but you can also just do `#[tokio::main(flavor = "current_thread")]` to get a single threaded executor.

More to the point, even using a multi-threaded runtime won't spawn threads willy-nilly. It will default to using N worker threads (where N is the number of CPU cores available).

Re: Local async executors and why they should be the default

#76

For me the biggest issue with Async is the management of multiple dependent async calls. It has some weird thing going on and I am not sure which pattern to use exactly. Some functions expect exactly same async fn signature some not and I am not sure why and which one to use.

I'm confused what you mean here.

If you have a function that "depends on" another function you call it within the other function. If it's async you .await it. Do you mean something about spawning tasks or passing callbacks around?

Re: Local async executors and why they should be the default

#77
post #42

Earlier quoted context omitted.

Your CRUD web application server almost certainly doesn't need async Rust. Using a blocking HTTP server is not "might be a good idea", it simply is a good idea. I recommend Rouille for this: https://github.com/tomaka/rouille . In case you are worried about performance, check the benchmark. Blocking Rouille is faster than builtin async server in Node.js.

>Your CRUD web application server almost certainly doesn't need async Rust. Using a blocking HTTP server is not "might be a good idea", it simply is a good idea. How so? By what logic? > Blocking Rouille is faster than builtin async server in Node.js It isnt proof

Because there are no advantages and only disadvantages of using async Rust. Async Rust is harder to use, and by assumption you don't need async Rust performance.

Re: Local async executors and why they should be the default

#78
post #25
post #15

While the article mostly focuses on the cognitive cost, which I deeply sympathize with, I do wonder about the runtime performance cost. Are there any good benchmarks actually quantifying the impact of all that extra thread-safety and the hoops that it adds? I'm not asking simply due personal interest in seeing the numbers, but also because if we want to steer the community towards this non-threadsafe direction it wou…

Anecdotal evidence, but many large rust async code bases perform significantly better if you are using a single threaded runtime vs. a multithreaded runtime. Here is an issue for the quinn crate that implements QUIC: https://github.com/quinn-rs/quinn/issues/1433 We have had very similar experiences when developing https://github.com/n0-computer/iroh And this is with quinn still carrying around the synchronization pri…

> Send and lifetimes just does not play well together.

That's the point!

Re: Local async executors and why they should be the default

#79
post #59
post #36

Earlier quoted context omitted.

Your dependencies are what you choose them to be, you are not forced to use async libraries if you don't want to.

Technically true, but in practice you’re constrained by whatever is provided by the ecosystem. And the truth is that many Rust projects were forced to choose async or sync, so these are often disjoint and fragmented. Even within async, crate authors need to pick and choose which async ecosystem to support, due to lack of support in std for spawning tasks and running simple IO, which most projects need. This is slight…

Libraries don't just sprout out from nothingness, someone has written those. And if there isn't a library that suits you, that someone can be you then. Key observation is that the ecosystem is only additive, it doesn't deny you of anything.

Re: Local async executors and why they should be the default

#80
post #32

In C# i always wondered why they couldn't hide the async/await logic for most cases. I never need to fire off two IO futures at the same time, so just make the thread do other stuff if i'm waiting for IO feedback, don't make me type out async/await in all impacted functions, let the compiler figure out when it can process other stuff

the use of async and await is a design decision that requires knowledge of the program's logic and desired behavior. It's not just a matter of compiler optimization, and that's why the compiler can't automatically figure out where to use these keywords. Suppose we have a service where users place orders, and we need to: 1. Save the order. 2. Deduct items from inventory. 3. Send a confirmation email. If we perform the…

You have it backward. The compiler should implicitly add the awaits for waitable objects, unless an operation is explicitly async. So you would write (in pseudocode):

  Task PlaceOrder(Order order) { SaveOrder(order); DeductItems(order); SendConfirmationEmail(order); }
And the compiler will implicitly await all three operations (and ideally infer that your function is async).

If you want to overlap computation, you avoid the implicit wait with async:

  Task PlaceOrders(Order order1, Order order2) { 
       let done1 = async PlaceOrder(order1); // async prevents implicit waiting
       let done2 = async PlaceOrder(order2);
       wait_all(done1, done2); // wait_all is also async and implicitly awaited. Ideally this should happen automatically for all unwaited and not returned futures at end of scope
   }
This allows being polymorphic on the async-ness of the function (pardon the pseudo c++):

   template F >
   void for_each(R range, F f)  {
       for (auto x : range) f(x); // f(x) is awaited if f is async and for_each itself becomes async.
   }
edit: sometimes it is important that no preemption happens in a region [1], so some scoped marker (atomic { ... } for example) would case a compilation error if an await would be introduced automatically.

edit2: and of course you should be able to use async even if the called function is a boring old blocking one. The runtime can spawn background task (or better yet use work-stealing) to run it.

[1] personally I think that atomicity guarantees should be about data, not code, but whatever.

Post reply on HN