Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> 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 synchronization primitives. Those primitives aren't really "thread"-specific if by thread you mean OS thread. When you do async like this, you're basically re-implementing OS threads in user space.



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 use not just concurrency but full blown multithreading primitives.

So e.g. you have to use Arc<RwLock<...>> where a Rc<RefCell<...>> would suffice.

Now, to be fair, both futures and tokio support local futures. There is boxed_local (https://docs.rs/futures/latest/futures/future/trait.FutureEx...) and tokio-util even comes with an optional local task pool (https://docs.rs/tokio-util/latest/tokio_util/task/struct.Loc...).

But these features are hidden and certainly not promoted in examples and documentation.


> So e.g. you have to use Arc<RwLock<...>>

In terms of C++ code that would equate to std::shared_ptr<std::shared_mutex<...>> 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.


The lengths that the Rust devs went through to minimize the overhead of async/await is impressive but also horrifying. And unfortunately, async/await certainly isn't a zero-overhead abstraction if you consider ergonomics/learnability. I wonder if it can evolve without breaking backwards compatibility.


Not sure what you mean by "returning a rust promise". But yes, Arc<Mutex<...>> is a thread safe smart pointer containing a mutex, which has quite some overhead.

You will see this in many places in the internals of async code that has to be Send.

OK in many cases, but it can kill you if you have to go through this for many small operations.


Yes, that's what I meant - it's a heavy operation consisting of multiple dynamic memory allocations and atomics. Former can be somewhat addressed through custom allocators and which in my case proved to be a big win for small and short-living tasks.


> Why is it not possible to simply return a Rust promise?

It… is? An Arc<RwLock> is what you need to share mutable data between “promises”.


I understand that part. What I don't understand is why https://docs.rs/promises/latest/promises/struct.Promise.html are not used instead.


Because there is no “instead”? A promise (or really a task if you’re in an async context, what you link to is really a thread + a pseudo-oneshot channel) allows independently scheduling a unit of work, it’s neither shared nor mutable state.

The promise object you link to is not Clone, so there is no way for two different threads to refer to the same one without adding an intermediate `Arc`, and because all the methods on it operate by value that’s not exactly helpful either as you can’t move the promise out of the Arc.

Same with a tokio::task::Task. A JoinHandle can’t be duplicated (though according to its doc on some platforms it might be duplicatable — if the thread returned a duplicatable value anyway).


I believe you can just annotate your tokio::main with flavor = "current_thread"


I think the article's point is that if you're doing single-threaded concurrency then you don't (or at least might not) need synchronization primitives. If you're writing a Node script, for example, then you don't have to worry about two callbacks trying to modify the same variable at the same time because you know that there is only one thread of execution.


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.


True. Rust's mandate for "zero runtime cost abstractions" is one of the better justifications for async/await. Because async/await, too, has significant limitations, notably:

1. Coloured functions

2. Placing the concurrency design decision (async vs not) on the implementer of a function, not the caller

3. Debugging complexity

(1) and (2) are mutually, negatively, reinforcing. As the implementer of a library function, I can't possibly know all the possible scenarios in which my function will be called. If I make it synchronous, then I'm blocking my caller. Which might be an issue - or might not. If I make it async then I don't block the caller - but I've just consigned the entire call stack to be async.

There's clearly strong opinions on both sides. Some are strong advocates of async/await. Personally, I'm not. Multiple, concurrent, fine-grained sequences of actions fit my mental model much better. Async/await doesn't give me that: I find reading async/await code to be a bit like reading control flow with GOTOs.

My preferences are undoubtedly influenced by working with Erlang, which has supported fine-grained sequences since the beginning. There are no coloured functions; just functions. As the caller, I can decide if I want things to run sequentialy (call from another function) or concurrently (spawn as a separate Erlang process). I have the context I need to make that decision.

It's not a panacea by any means (see e.g. Structured Concurrency [0]). But it fits my mental model much better.

YMMV of course.

--

[0]: https://en.wikipedia.org/wiki/Structured_concurrency


I'm not fan of async, but I don't like to deny reality by saying just use Structured Concurrency/Green threads/Magic Multithreading Powder. They all have their trade-offs.

And to be fair they are working to solve Colored functions.

However, async is still half-baked because no support for async traits and other corner cases.

Which was again caused by async being in RFC, development hell.


And then everyone has to bundle tokio anyway.


Tokio might is not a panacea either. If they pulled it in std, backwards compatibility would kill most evolution.


At a certain point, one needs to question evolution at any price.


Sure, but I honestly I think std should have things you DON'T want to evolve. Stuff like time/chrono. And YAML.

Pretty sure Rust developers want to move std into crates as well, as well as parts of compiler. And I completely understand them.


Waiting for I/O does not require using multiple threads. You can have the same thread do other things until I/O is ready.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: