Why should you use Tokio vs. threads in Rust?
twitter.com
twitter.com
In my opinion performance is always relative and dependent on the workload. Instead it’s worth looking more at which capabilities async Rust provides which aren’t present in non-async Rust. This includes ad-hoc cancellation, ad-hoc concurrency, and combining the two into things such as timeouts.
I wrote a post explaining this in detail a few months ago: https://blog.yoshuawuyts.com/why-async-rust/
If you're serving a single connection with 1 thread then you're wasting that thread unless you do enough computation to fill that logical hardware thread's capacity.
In other words it's a good idea to use both together.
I wrote a 1:M:N scheduler in Rust, C and Java. It multiplexes N lightweight threads over M kernel threads and has 1 scheduler thread.
https://GitHub.com/samsquire/preemptible-thread
Nginx has multiple worker processes and an event loop in each I think.
There's no reason your event loop couldn't be on each thread.
I'm also thinking of separating recv and send on different threads so you can send and receive simultaneously.
Loom seems to be a new implementation of m:n Threading for Java. How does your implementation compare?
---
Could not find references from 20 years ago, but this paper from 2014 is nice:
"Earlier M: N was considered best among all because of its performance benchmarks. Currently the Linux and Solaris have shifted from M: N to 1:1 thread model despite of the advantages of M:N. Our emphasis will be on 1:1 Model semantics for shifting. This paper gives a thorough review of operating system thread models. We have researched factors which compelled different OS platforms to shift to 1: 1 Model."
https://www.researchgate.net/publication/270217184_Thread_mo...
Kotlin successfully introduced green threads (viz. coroutines) to the JVM world way back when. Kotlin constructs for structured concurrency are especially super nice.
I am wondering whether to include S for socket in the syntax for scheduling.
So if you have M:N:S multiplexing you can share a socket over multiple lightweight threads which are themselves assigned to one kernel thread at a time.
My implementation is a toy compared to Loom. Loom is a serious API adaptation which causes the Thread API to work as lightweight threads, I am not sure if it handles process starvation as my M:N scheduler does.
if you put while (true) {} in a lightweight thread, I think that thread shall either be preempted by Loom if Loom implements that or it shall keep the lightweight thread assigned to that kernel thread, causing starvation of other lightweight threads.
So you might have
thread1:light1:socket1recv thread2:light2:socket1send thread1:light1:socket2recv thread1:light2:socket2send
I believe that Loom is the first official M:N thread scheduling implementation for the JVM.
Go's M:N Threading is a great success. I guess M:N threading is harder to implement at the OS Level.
(1) https://en.wikipedia.org/wiki/Native_POSIX_Thread_Library
I find it fascinating that LWT have come so far that this is a legitimate question. There was a time when async << threads << processes. But a lot of heroic good work went into bringing about this wonderful age.
How does that simplify things in rust? You don't get shared mutability either way, and RefCell is barely simpler than Mutex.
I rewrote the same thing in Golang afterwards, took me maybe 10 % of the time and the code I produced was very readable and extremely fast. After that I'm a little burnt regarding Rust and have a hard time buying all the hype around it. I guess it has potential for embedded systems and other memory-constrained environments though, as Golang can't compete there right now due to the large size of the runtime, though there are projects like Tinygo that might eventually fill that gap as well. So I'm probably more hyped about Zig than Rust for now when it comes to next-gen languages.
The compiler is quite good at catching some issues relating to memory access predominantly. There's many ways to solve multi-threading issues (&, &mut, Rc, Box, Arc, ...).
If you do it the right way the compiler won't get in your way much, but if you do it the wrong way it will be a cat and mouse game where you do your best to confuse the compiler into believing your code is fine-ish.
Given the one consistent thing I hear about Rust is it's steep learning curve, I'd almost certainly chalk it up to that. Go let's you ignore details that you can't ignore in Rust, and being forced to think about and organize data in a certain way to eliminate that pain is a skill that no doubt takes practice.
It's a shame that your C or C++ experience doesn't translate to Rust. For me a lot of things just clicked in Rust. Although there certainly was the occasional "why can't I just do this?" that took much longer to reason all the way through. It's maybe something Rust tutorials need to be better at, or maybe there ought to be tutorials specifically for e.g. C++ folks?
For better or worse, Rust gives you the choice (& vs Arc, etc), and it's yours to make. These choices are fatiguing to make if you aren't used to them (I believe C/C++ is similar for newcomers). In the long run, making those choices explicit is where Rust really shines in codebases IMO.
Nothing wrong with using Golang if it works for you. I sometimes wish for a middle ground between Python and Rust, but Golang isn't for me.
Last I checked, the kernel allocates a bunch of address space per thread but it doesn't consume actual memory unless you touch it and page fault.
Tying your memory consumption heavily to the max stack depth also seems like it would be difficult to reason about IMO.
- Arcing everything
- using a fixed-sized ancillary array and passing indices
- creating threads that correspond to the lifetimes you care about
- fighting your way through adding explicit lifetime allocations and
- delaying reclamation of _everything_ above it on the stack
...
and while workable, none of those things are particularly niceThat’s mapped space, so depending on the OS it could be more or less friendly to its memory subsystem. Linux won’t bother finding you actual resident pages until you do need them. But if the stack drops back down, it will be swapped out as any other dirty memory. Rust does not do any MADV_DONTNEED magic behind your back to aid with reclaiming memory.
One common thought I see a lot though is that each thread uses 1MB (or more) in stack space alone. This just isn't true with modern memory paging
I want to pick N cores for I/O and the rest to do compute but I didn’t see a way to let me pin the CPUs rayon could use for its compute threads.
[0] https://docs.rs/rayon/latest/rayon/struct.ThreadPoolBuilder....
"If your tasks spend most of their time waiting, use async. If they spend most of their time running, use threads"
?
If one was to benchmark cpp coroutines vs rust async would the performance difference ever pay off?
The issue is that async generator codegen currently is not as good as we want it to be. Async futures currently take more space than they need to and have some issues with drop tracking. They're sound, but not necessarily efficient.
These are not problems with the language or the feature, just the current implementation