Rust has broadly the same concurrency support, with a more powerful compile-time race detector (but one that also comes with a learning curve). The main difference is that Go uses M:N threading, while Rust uses 1:1 threading with optional explicit async/await constructs.
Rust is really great, but I feel like you are overselling it a bit here:
- async/await is available in the nightly version of Rust (1.39), but not in the stable version (1.37).
- 1:1 threading is not strictly equivalent to M:N threading (the main practical difference being that the stack size per OS thread is larger than the stack size per goroutine).
Writing that "Rust has broadly the same concurrency support" is misleading right now. But it could be true in a near future.
The features that each language offers are the same: threads, channels, and blocking I/O (though Rust has more features for safe concurrency--Rust prevents data races statically, while Go is not even memory safe
in the presence of such races). What is different is the performance characteristics. In some cases, M:N will be more efficient; in some cases, 1:1 will be. But just as I wouldn't say Go is lacking FFI features because M:N makes cgo slow, I wouldn't say Rust is lacking concurrency features.
Rust I/O are strictly similar to Go when using 1:1 OS threading, but become conceptually different when using async/await.
> Rust has more features for safe concurrency--Rust prevents data races statically, while Go is not even memory safe in the presence of such races
Agreed. That's a big advantage of Rust.
> What is different is the performance characteristics.
Performance is a feature. It's significant enough to justify using one language or the other depending on the project at hand.
> In some cases, M:N will be more efficient; in some cases, 1:1 will be.
The main advantage of the M:N model, compared to the 1:1 model, is the memory usage, because each goroutine starts with a small stack (a few kB). It makes possible to start a larger number of M:N goroutines than 1:1 threads.
> But just as I wouldn't say Go is lacking FFI features because M:N makes cgo slow, I wouldn't say Rust is lacking concurrency features.
Go's FFI works but is slow. It's a well known fact. Rust's concurrency story is not stabilized yet (areweasyncyet.rs). It's a fact too. I don't see a problem with acknowledging both :)
For the threading difference, do you mean that Rust needs 'normal' threads plus async/await support, but Go can just lean on its built in concurrency support for both cases? i.e. Go threads instead of explicit async style code.
Your sentence implies that Go green threads can do both of what 1:1 threading and async/await can, but it's more complex than that: there's a tradeoff between the simplicity (only one concurrency primitive) and the capability of the said primitive:
- with it's M:N model, Go cannot really do FFI efficiently, while both 1:1 async/await have no problem with that in Rust.
- goroutines are cooperatively scheduled, while OS thread are preemptively scheduled. If you have hot loops, Go's model won't be able yo guarantee fairness between goroutines, which may be a problem depending on your usage. OS threads don't have this problem.
- with goroutines you won't achieve the level of performance you can reach with async/await.
Go and Rust fills a different niche of programming and take a different stand on this tradeoff: Rust gives you more power at a complexity cost. Go offers you more simplicity, with less power.
The reason cgo is (relatively) slow is small stacks, not M:N threading.
Small stacks (and M:N threading) are needed to efficiently implement tens of thousands of goroutines.
CGO is slow mostly because it needs to switch to a larger stack when calling a C function.
It's a trade-off.
A different Go implementation could make Goroutines map 1:1 to threads and have fast cgo calls at the expense of slow goroutines.
Rust is not exempt from those trade-offs. They chose fast FFI and smaller runtime. They paid with slow threads.
Async/await promises to be the best of both worlds but it comes at a cost of great complexity, both for the programmer and the implementor.
At the end of the day under the covers it's just threads that need to be managed in a complex and often invisible way plus a complex rewrite of your straightforward code into a mess of a state machine.
I can confidently say that learning to use goroutines took 10x less time than learning async/await in C#.
I understand goroutines better than I ever understood async/away.
> Small stacks (and M:N threading) are needed to efficiently implement tens of thousands of goroutines.
Tens of thousands of threads are no problem with 1:1 threading on Linux.
> They paid with slow threads.
I would not call 1:1 threads "slow threads". If they were slow, then the 1:1 NPTL would have not defeated the M:N NGPT back in the day when this was being debated in the open source OS community.
> complex rewrite of your straightforward code into a mess of a state machine.
The entire point of async/await is that you don't have to do a complex rewrite of your straightforward code. It remains straightforward, and the compiler does the transformation for you.
Given the context, it's not a memory problem was implied, because small stacks are not actually relevant, because C stacks are not committed upfront (at least on unices, not sure about linux). So your threads are more likely to take up a page each (plus kernel datastructure overhead) than the 8MB allowed to a C stack.
> I can confidently say that learning to use goroutines took 10x less time than learning async/await in C#.
It's a matter of preferences, I've always preferred async/await to threads because you don't need to juggle with channels and you're way less likely to cause a deadlock.
> it comes at a cost of great complexity, both for the programmer and the implementor.
Is that really harder to implement than a M:N runtime like Go's?
> The reason cgo is (relatively) slow is small stacks, not M:N threading.
You're picking nits here. Without small stacks there would be no point using M:N threading in Go…
And Cgo is not only relatively slow: most of the time it is slow enough to destroy all performance benefit of calling a FFI function. It's a pretty big deal.