Live data from Hacker News

Local async executors and why they should be the default

maciej.codes

81–90 of 327 posts

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

#81
post #69

Earlier quoted context omitted.

I was thinking more in terms of making good old 'blocking' calls and that the compiler can 'taskify' those old blocking calls to let the thread do some other task instead of waiting on the 'blocking'

Sometimes you want to spawn a [future/task/promise/...] and not immediately await it, but instead pass it around. Suppose something like: should_be_future = async_function() should_execute = async_function() # While the compiler could figure out where to insert awaits automatically, it adds cognitive overhead for the developer -- suddenly the same way of calling a function can result in either a [future/task/promise/…

Then you should annotate the tricky diverging call site with async (see my other comment), instead of making it implicit and requiring annotation of the common expected case:

    should_be_future = async async_function()
    should_execute = async_function() # 

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

#82
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/Ce…

There is quite some support for the second option. But it is not advertised at all.

E.g. there is an entire local task pool impl in tokio_util, but it is not enabled by default and at least for me was difficult to find. https://docs.rs/tokio-util/latest/tokio_util/task/struct.Loc...

I wrote an entire local executor pool before finding that thing and ripping my code out again...

At the very least the support for tasks for non-Send futures should be advertized more, and maybe also the APIs polished a bit.

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

#83
post #39
post #35

I find all the async stuff in Rust incredibly ugly, cumbersome, and its one of the biggest reasons I prefer C++, still. C++ lets me just write single- or multithreaded code, because none of the dependencies force their `async` stuff on me. Yeah, its up to me to ensure things are synchronized, but I'd rather do that than try to figure out how to get some dependency that isnt meant to use async to work in some async mo…

I can see where you are coming from. But you can do most things purely in sync rust. There are sync rust multithreading libs like rayon that are a joy to use, and there are even blocking versions of popular http libraries like reqwest: https://docs.rs/reqwest/latest/reqwest/blocking/index.html They are usually doing some ugly stuff internally to make this work, but as a pure library user you don't have to care. If yo…

Can you expand what ugly stuff they're doing internally? I mean, synchronous code is the most simple way to do anything. POSIX API is synchronous by default. Why would you need ugly stuff? Asynchronous API might force you to do ugly stuff (e.g. for files or DNS).

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

#84
post #42

Async is and probably will always be less usable than blocking Rust. It is a very, very useful mode of operating when you really need two of its biggest benefits: lightweight cooperative concurrency and task cancellation, but it comes at a big usability cost. Rust software should use async tactically - in places where it is needed. Unfortunately handling http, which is a large part of many applications is actually a…

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.

Thanks for this reference. Will bookmark for if I ever need an HTTP service.

Wish this also provided websockets.

EDIT: it does https://github.com/tomaka/rouille/blob/master/src/websocket/...

Looks a bit neglected though. Not much recent development.

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

#85
post #73
post #17

Earlier quoted context omitted.

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.

And green threads aren't a panacea. They introduce runtimes, which was something Rust avoided on purpose.

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

#86

"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 thread…

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

Doing something extra to get a behavior different than default is the definition of opt-out my friend.

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

#87
post #39

Earlier quoted context omitted.

I can see where you are coming from. But you can do most things purely in sync rust. There are sync rust multithreading libs like rayon that are a joy to use, and there are even blocking versions of popular http libraries like reqwest: https://docs.rs/reqwest/latest/reqwest/blocking/index.html They are usually doing some ugly stuff internally to make this work, but as a pure library user you don't have to care. If yo…

Can you expand what ugly stuff they're doing internally? I mean, synchronous code is the most simple way to do anything. POSIX API is synchronous by default. Why would you need ugly stuff? Asynchronous API might force you to do ugly stuff (e.g. for files or DNS).

Let's say you are using a low level networking library that is async to the core, like hyper or quinn.

Then you will have some ugliness at the boundary. You might get by with just spawning a current thread runtime to use your async lib, or do something like blocking channels if your low level library spawns long lived tasks.

If your low level libraries are all sync, then there won't be much ugliness.

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

#88
post #35

I find all the async stuff in Rust incredibly ugly, cumbersome, and its one of the biggest reasons I prefer C++, still. C++ lets me just write single- or multithreaded code, because none of the dependencies force their `async` stuff on me. Yeah, its up to me to ensure things are synchronized, but I'd rather do that than try to figure out how to get some dependency that isnt meant to use async to work in some async mo…

> but I'd rather do that than try to figure out how to get some dependency that isnt meant to use async to work in some async move closure

tokio::spawn_blocking[1], see it's not that hard. But sure to use another language you must learn it, an it requires a bit if effort…

[1] assuming you want to use tokio like post people do, but other executors should have the same kind of functions to do that as well if need be.

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

#89
post #35

I find all the async stuff in Rust incredibly ugly, cumbersome, and its one of the biggest reasons I prefer C++, still. C++ lets me just write single- or multithreaded code, because none of the dependencies force their `async` stuff on me. Yeah, its up to me to ensure things are synchronized, but I'd rather do that than try to figure out how to get some dependency that isnt meant to use async to work in some async mo…

I find the async stuff obnoxious, too, and I avoid it. Helps that I don't work in the web space.

But in general the dependency story with Rust is better than with C++. I don't miss the integration story with C++; just bringing in a dep in the first place is a roll of the dice on whether it's going to work with your build system. And then whether it brings with it some other lifestyle assumptions (exceptions, some third party deps you don't want or can't link to, work with your compiler or not, etc.)

Honestly, I work in systems & embedded stuff, and it's not hard to avoid async in Rust. It's only once you start poking at web-adjacent stuff that you run into the wall, and then when you do you find yourself covered in the taint of people who brought their NodeJS Stockholm Syndrome over to Rust, but that's another rant...

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

#90
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/Ce…

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

Honestly I don't think every user want top notch throughput when writing asynchronous Rust applications. Most users would just want the correctness of the Rust type system, and its "lightweight" runtime characteristics compared to CPython, Node.js etc., which can provide fairly good performance.

The thing is, using Arc in single threaded runtime does not greatly harm performance. If it matters, you should be handling 1M+ rps per core and using multithreaded runtime because it scales better, which benefits from using threadsafe primitives.

Post reply on HN