The future of M:N threading
mail.mozilla.org
mail.mozilla.org
[1] https://github.com/mozilla/rust/graphs/contributors
EDIT: To whoever removed the [Rust-dev] annotation from the title of this submission, please put it back. It adds valuable context, and it's in the title of the page itself, so you're violating HN's naming policy by removing it.
Seeing a link to [mozilla.org] with talk about threading, I assumed it was something to do with Firefox (which I care about) versus Rust (which I don't).
This decision probably puts it closer to C++, but I can't see how that is a good thing.
Making M:N threads a first class POSIX feature is a good starting point as long as it also allows you to express the intended locality of groups of threads. Locality isn't optional anymore. The slowdowns he reports also map to scale up issues you see when you don't think about keeping frequently accessed mutable data local and shared nothing to an OS thread.
M:N threads are just an abstraction around a stack and registers that frees you from having to bind up a bunch of related state into a series of objects and maps. It's awesome, I want it, but nothing is quite there yet. At least not if you want something like Java or C/C++ performance.
Work stealing across cores is really nice, but I am not sure it is always the behavior you want. A programmer can tell when migrating a task across cores is going to hurt more than waiting a little longer to get to it.
I also think any discussion of M:N threads or multi-threading is also incomplete if it doesn't also include garbage collection as a means of passing data between threads. Right now we have languages that are garbage collected and treat native memory as a second class citizen and non-GC languages that make multi-threading a headache.
GC is great for multi-threading, but not so great for hundreds of gigabytes of variable lifetime objects. Even if the GC can handle it, GC still has unacceptable 2x space overhead.
I don't want much :-)
The youtube video doesn't load for me :-(
Not many languages can afford doing that and of course there may be lots of features that prevent similar behaviour.
So the simple and effective GC comes at the cost of a limited programming model. For Erlang, it's the right tradeoff, but not for every environment.
> It drops further when asked to allocate 8MiB stacks like C is doing
Memory, just like CPU is constrained and costs money especially if provisioned in the cloud. If I have 2G of memory available I can spawn 250 threads and keep them around without swapping.
EDIT: actually scratch the above, _wmd pointed out that stack memory is just like virtual memory, it is allocated but brought into physical memory as needed.
This changes how code is written. It is not just an internal detail! Now you cannot think "one-connection = one-task" now it is about locks, queues and mapping control blocks to connections. That is the biggest loss.
> GC is great for multi-threading, but not so great for hundreds of gigabytes of variable lifetime objects. Even if the GC can handle it, GC still has unacceptable 2x space overhead
Good point on GC. I see the main issues in a large concurrent system (and presumably Rust wants to be a good tool for concurrent systems) is non-blocking GC. A slow GC might be acceptable, but if one errant task allocated memory in certain pattern and GC has to lock all the tasks out to run, that could be a serious issue.
Responsiveness (liveliness) of a concurrent system is something that is often forgotten and everyone wants to talk about how fast one can transpose a matrix.
Now a concurrent GC is possible, Azul built one, for Java, it is very interesting how it work. I enjoy reading their whitepapers:
Thread stacks are virtual memory like everywhere else, so in reality an "8mb" stack means "8mb maximum size". Sure, they won't shrink once pages are faulted in to back them, but in the average application, especially on 64bit, this should never be a problem
If for the lifetime of your thread, its stack only ever grew to 32kb, then the OS will only have allocated 32kb of memory to back it.
It varies by workload, but for mine that is between 128 and 512 kilobytes per thread.
If you churn all your threads no problem. If you are hosting 100k persistent connections I suspect it would detract from available memory.
I expected the linux kernel to be more intelligent, but maybe there's a reason it behaves like that?
Though I can't think of a sane way to call madvise(2) involving a thread stack
That's not to say threads aren't expensive, they still require several heavyweight struct allocations on the kernel side, e.g. struct task_struct, which I counted to 1k before getting bored (and wasn't even quarter way through the fields)
Edit: Linux git HEAD with Debian unstable .config:
(gdb) print sizeof(struct task_struct)
$1 = 1904
I think Rust is still pretty interesting. The borrow checker is extremely intricate and is completely unlike anything seen in any industry language so far.
For example?
I think Rust did a good job of making a non-garbage-collected language make memory management less of a headache in a multithreaded scenario (though I'm biased of course). In fact, I think it works even better than in languages with global GC.
By default you transfer memory between threads ("don't communicate by sharing memory, share memory by communicating"), and the compiler enforces that you can't touch memory after you've given it away. You can also share read-only data (Arc), and if you do that then the compiler will make sure you can't mutate it and race. If you want to use locks, you can use those too (MutexArc), and the compiler will ensure you take and release locks properly. No tracing garbage collection in sight, and no fiddling with race detectors at runtime—the compiler does the work for you.
To me it seems it is possible to do M:N right, but you need more abstraction and a different design. M:N seems to work well in Erlang-like cases (very lightweight processes on top of a kernel threads).
> Over the past year we've been looking at how a uniform, performant, synchronous programming model can be implemented across all of Google's core languages (C++, Java, Go, Python), while maintaining compatibility with existing code.
So yeah, if you have a really good runtime you can see some significant gains on some systems and workloads. More often you'll get very modest gains, and you'll have some extremely subtle bugs that cause a precipitous drop in performance e.g. when you start paging. That's "ending in tears" for the poor schlep who has to debug that stuff. It can be done but, as I said, it's not the best bang for the buck.
Not always: http://www.haskell.org/ghc/
"Over the past year we've been looking at how a uniform, performant, synchronous programming model can be implemented across all of Google's core languages (C++, Java, Go, Python), while maintaining compatibility with existing code."
How would this affect the performance of applications that spin up thousands of tasks?
The M:N model is notoriously difficult, making your threading system 10x as complicated usually isn't worth the 10% performance increase. Haskell is, again, the exception that proves the rule. In Haskell there is no stack and no need for TLS, so the complexity of M:N threading is much lower. Additionally, on Haskell there is a provision for pinning green threads to OS threads if you need interoperability with foreign code that can't get moved across threads as easily, and the assumption is that you usually don't need to pin threads.
I'd also like to point out that for some applications, using native threads instead of green threads will reduce the number of context switches, it's when threads are CPU bound that green threads give you the biggest performance boost.
You can do that in Rust too (and we do it a lot).
The problem that created the whole non-blocking, async domain was the memory needed for the thread's call stacks. You don't have enough address space to place a bunch of 2MB call stacks for each thread, if you're handling tens of thousands of connections.
c10k is (almost) 15 years old, updating it for today are the OS and MMU going to manage 10 million threads and stacks for that many connections?
Mine are 10MB:
$ ulimit -s
$ 10240
I can only spawn 200 threads in the worst case and keep them active before system starts swapping. Yes memory space is large by anytime those have to run they'd have to be brought up into the working set.EDIT: nevermind, _wmd pointed out that stack memory is still virtual and it only be brought into the physical memory as needed.
Is this not true?
I am stupid. I'll correct the initial comment.
Thanks
http://en.wikipedia.org/wiki/Scheduler_activations
http://web.mit.edu/nathanw/www/usenix/freenix-sa/freenix-sa....
You can run an lthread scheduler per core.
[1] http://akka.io/
Another option is to do it at the language level - see my comment on Erlang.
An example would be Erlang which is a runtime built from the ground up to support lightweight threads where reading or writing to a socket of using the built in APIs won't block the underlying OS thread. You can still sabotage the runtime by writing your own native library and doing blocking operations from there, but the pitfalls are more obvious in that scenario.
If the underlying concurrency and network/disk IO primitives are truly non-blocking for lightweight threads then everything built on top of them will also be non-blocking. Non-blocking for the underlying OS thread that is.
That is where co-routines and other co-routine like libraries are a leaky abstraction. If you step into one of those blocking APIs in a third party or standard library you will block the underlying OS thread. This means you can't integrate with existing code or tools easily and you are always vulnerable to accidentally doing so.
If this can't be done, then that brings me back to my previous point. It seems like the important thing is that you have non-blocking IO and not that you have an M:N threading model in the runtime. If you have non-blocking IO then it seems like you could easily implement an M:N threading model as a library without making it awkward to interact with.
Is there some assumption I'm making that doesn't make sense?
> If you have non-blocking IO then it seems like you could easily implement an M:N threading model as a library without making it awkward to interact with.
Yes, but your language/system has to have some mechanism to manage control flow. Examples: F# does what you ask for via asynchronous workflows. Ruby via fibers/EM-Synchrony. Java via a bytecode weaver like Kilim.
I'm surprised by this, I would have expected that voluntary preemption saves you from having to save the FP registers as well. Is the absence of the optimization a non-x86 thing?