Comment by unscaled
4 days ago
You can argue about how likely is code like that is, but both of these examples would result in a hard compiler error in Rust.
A lot of developers without much (or any) Rust experience get the impression that the Rust Borrow checker is there to prevent memory leaks without requiring garbage collection, but that's only 10% of what it does. Most the actual pain dealing with borrow checker errors comes from it's other job: preventing data races.
And it's not only Rust. The first two examples are far less likely even in modern Java or Kotlin for instance. Modern Java HTTP clients (including the standard library one) are immutable, so you cannot run into the (admittedly obvious) issue you see in the second example. And the error-prone workgroup (where a single typo can get you caught in a data race) is highly unlikely if you're using structured concurrency instead.
These languages are obviously not safe against data races like Rust is, but my main gripe about Go is that it's often touted as THE language that "Gets concurrency right", while parts of its concurrency story (essentially things related to synchronization, structured concurrency and data races) are well behind other languages. It has some amazing features (like a highly optimized preemptive scheduler), but it's not the perfect language for concurrent applications it claims to be.
Rust concurrency also has issues, there are many complaints about async [0], and some Rust developers point to Go as having green threads. The original author of Rust originally wanted green threads as I understand it, but Rust evolved in a different direction.
As for Java, there are fibers/virtual threads now, but I know too little of them to comment on them. Go's green thread story is presumably still good, also relative to most other programming languages. Not that concurrency in Java is bad, it has some good aspects to it.
[0]: An example is https://news.ycombinator.com/item?id=45903586 , both for the same article.
Rust has concurrency issues for sure. Deadlocks are still a problem, as is lock poisoning, and sometimes dealing with the borrow checker in async/await contexts is very troublesome. Rust is great at many things, but safe Rust only eliminates certain classes of bugs, not all of them.
Regarding green threads: Rust originally started with them, but there were many issues. Graydon (the original author) has "grudgingly accepted" that async/await might work better for a language like Rust[1] in the end.
In any case, I think green threads and async/await are completely orthogonal to data race safety. You can have data race safety with green threeads (Rust was trying to have data-race safety even in its early green-thread era, as far as I know), and you can also fail to have data race-safety with async/await (C# might have fewer data-race safety footguns than Go but it's still generally unsafe).
[1] https://graydon2.dreamwidth.org/307291.html
[dead]
Async and concurrency are orthogonal concepts.
While I agree, in practice they can actually be parallel. Case in point - the Java Vert.x toolkit. It uses event-loop and futures, but they have also adopted virtual threads in the toolkit. So you still got your async concepts in the toolkit but the VTs are your concurrency carriers.
But Rust's async is one of the primary ways to handle concurrency in Rust, right? Like, async is a core part of how Tokio handles concurrency.
Could you give an example to distinguish them? Async means not-synchronous, which I understand to mean that the next computation to start is not necessarily the next computation to finish. Concurrent means multiple different parts of the program may make progress before any one of them finishes. Are they not the same? (Of course, concurrency famously does not imply parallelism, one counterexample being a single-threaded async runtime.)
3 replies →
But even so, the JVM has well-defined data races that may cause logical problems, but can never cause memory issues.
That's not the case with Go, so these are significantly worse than both Rust and Java/C#, etc.
What is your definition of memory issues?
Of course you can have memory corruption in Java. The easiest way is to spawn 2 threads that write to the same ByteBuffer without write locks.
And you would get garbled up bytes in application logic. But it has absolutely no way to mess up the runtime's state, so any future code can still execute correctly.
Meanwhile a memory issue in C/Rust and even Go will immediately drop every assumption out the window, the whole runtime is corrupted from that point on. If we are lucky, it soon ends in a segfault, if we are less lucky it can silently cause much bigger problems.
So there are objective distinctions to have here, e.g. Rust guarantees that the source of such a corruption can only be an incorrect `unsafe` block, and Java flat out has no platform-native unsafe operations, even under data races. Go can segfault with data races on fat pointers.
Of course every language capable of FFI calls can corrupt its runtime, Java is no exception.
4 replies →