The comparison offered by the OP was Java 1.4 to Go 1.4.
I think the standard library is debatable (see the infamous early Java Date class), Java encourages using inheritance (so not composition instead of inheritance) - something Gosling later remarked as his biggest regret, and concurrency had improved tools in Java 2, not sure the story was as good in early Java.
> Also, to be very clear, Java's threads ("green threads") were _originally_ M:N but they switched back to 1:1.
Java's "green threads" were N:1 threads, not M:N threads. While both are sometimes referred to as "green threads", they are very different things. (N:1 tends to give cheap concurrency but no parallelism, M:N tends to give cheap concurrency with as much parallelism as the hardware can handle, but with more overhead than 1:1 threads.)
I don't know if the programming model of threads and channels that's used by haskell and golang strictly depends on the existence of M:N threading, but it sure is nicer to use than the model in java/rust.
It wasn't immediately obvious from scanning that link. Does it provide channel select? Because that's the hard part. Threadsafe queues are easy, select on them is hard.
It doesn't strictly depend on it, but it is strongly supported by M:N threading since that threading model decouples the supportable degree of concurrency from the supportable degree of parallelism (unlike 1:1), without abandoning parallelism (unlike N:1).
You can do Erlang-style concurrency with 1:1 or N:1 threading models (and there are libraries for languages whose many implementations are N:1 or 1:1 rather than M:N that do that), but it makes most sense when you have M:N.
> It doesn't strictly depend on it, but it is strongly supported by M:N threading since that threading model decouples the supportable degree of concurrency from the supportable degree of parallelism (unlike 1:1), without abandoning parallelism (unlike N:1).
A modern Linux kernel has excellent thread scalability, and you can customize thread stack sizes to improve memory usage. (Thread memory use is kind of independent of 1:1 vs. M:N, honestly; the thing that can improve scalability is relocatable stacks, which is really not the same thing.)
It's instructive to look at the history of M:N versus 1:1 in the days of NPTL and LinuxThreads and compare that to the history of programming languages. In that world it was received wisdom that M:N would be superior for the reasons always cited today, but at the end of the day 1:1 was found to be better in practice, because the Linux kernel is pretty darn good at scheduling and pretty darn fast at syscalls. Nobody advocates M:N anymore in the Linux world; it's universally agreed to be a dead end. Now, to be fair to Golang, relocatable stacks do change the equation somewhat, but (a) as argued above, I think that's really independent of 1:1 vs. M:N; (b) any sort of userspace threading won't get you to the performance of, say, nginx, as once you have any sort of stack per "thread" you've already lost.
> A modern Linux kernel has excellent thread scalability, and you can customize thread stack sizes to improve memory usage.
Cross-platform code can't count on always running on a modern Linux kernel, though.
But, sure, I'd think that M:N (which is never free) is going to be less likely to be worth the cost if you are specifically targeting an underlying platform reduces the cost of high numbers of native threads and the cost of native thread switching so that the price for user-level threading in the runtime isn't buying you improvements in those areas.
Well, if what you're saying about not needing M:N to get thread scalability is true, I'm probably wrong. I really like being able to run tons of threads and write synchronous code, letting the the haskell RTS swap threads when I block.
My mental model, which probably comes from everyone complaining about apaches thread-per-request model a few years ago, is that you basically either consume lots of resources with lots of threads, or you write ugly cps style code. I view haskell/go as giving the best of both worlds, but perhaps they aren't separate worlds after all.
I feel the same about Erlang processes. It's so much easier on my brain to write synchronous code that's preemptively executed and communicating by message passing. Wish Rust gave me that option, speed hit and all, but I understand why it doesn't.
The choice of 1:1 and M:N does not affect the programming model at all. It is simply an implementation detail. M:N scheduling is no more a requirement for CSP than Unix is for TCP.
Maybe I'm using terms incorrectly -- is there a good green thread library for Rust which supports the kind of Erlangy experience I've described? I looked around a couple of months ago but didn't find anything that seemed suitable.
In my experience, kernel threads have a cost that is sufficiently far from that of, for example, Erlang processes (400+ bytes each) that it changes the way you program with them.
In Erlang you don't think twice about spawning a million threads if that models your problem nicely. The same isn't true when you're dealing with kernel threads. (Right?) So I'm interested in green threads because when I have to start thinking about the cost of the threads I'm using, then I'm thinking less about how to most elegantly solve the problem and more about how to satisfy the architecture I'm programming on.
Now, if kernel threads are massively cheap these days and there's no problem spawning a million of them, then I need to take another look at that model.
So since you're talking about scalability into the millions of threads, I think what you actually want is stackless coroutines rather than M:N threading with separate user-level stacks. If you have 1M threads, even one page of stack for each will result in 4G of memory use. That's assuming no fragmentation or delayed reclamation from GC. Stacks, even when relocating, are too heavyweight for that kind of extreme concurrent load. With a stackless coroutine model, it's easier to reason about how much memory you're using per request; with a stack model, it's extremely dynamic, and compilers will readily sacrifice stack space for optimization behind your back (consider e.g. LICM).
Stackless coroutines are great--you can get to nginx levels of performance with them--but they aren't M:N threading as seen in Golang. Once you have a stack, as Erlang and Go do, you've already paid a large portion of the cost of 1:1 threading.
Coroutines are preemptible at I/O boundaries or manual synchronization points. Those synchronization points could be inserted by the compiler, but if you do that you're back into goroutine land, which typically isn't better than 1:1. In particular, it seems quite difficult to achieve scalability to millions of threads with "true" preemption, which requires either stacks or aggressive CPS transformation.
A higher cost for context switching. Lots of people work on apps with lots of i/o, and there is a long history of coroutine/callback/green thread architectures beating the pants off of thread per request architectures.
No, the context switching overhead tends to be minimal if you're using a well-tuned kernel. You're doing a context switch to the kernel for I/O in the first place, and in a green thread model you have to do a userspace context switch in addition to the context switch the kernel imposes to get back into your scheduler.
yes
> implicit interfaces
no
> composition over inheritance
yes
> static builds
no
> built-in concurrency
sure, with an 1:1 thread model as opposed to an M:N thread model.
M:N is not always a clear win over 1:1:
https://mail.mozilla.org/pipermail/rust-dev/2013-November/00...
http://xiao-feng.blogspot.com/2008/08/thread-mapping-11-vs-m...
Also, to be very clear, Java's threads ("green threads") were _originally_ M:N but they switched back to 1:1.