Except that it doesn’t. @xuxinhang’s example is a complete program that exhibits no UB. There can be no race condition here, as the program is single-threaded. The example generalizes extremely poorly, but is not incorrect as written.
Though the example represents poor practice and deserves rebuttal, there’s no need to exaggerate its problems to make your point.
The value is synchronized by the inherent serialization of single-threaded execution in this case; there is no need for a runtime check.
That is a pretty weak argument for the "correctness" of the program. Although it currently holds technically, in practice this house of cards is going to implode the moment its strong and unfounded assumptions are violated. Therefore, with an overwhelming probability, using the proposed code outside its own very concrete wrapper program above will eventually lead to unsoundness. It isn't honest to expect otherwise, or to ignore the broader context.
I'm not exaggerating the problems, I am pointing them out, and apparently, for some reason that I don't understand, I now need to actively defend the "you shouldn't advise beginners to write bad code" standpoint.
Thinking more about it, I'm not sure the code is even technically sound. Although, as you correctly observed, it does not currently exhibit UB, it is possible to modify the example by only adding safe code, which does in turn result in UB.
Now, unsafe code is supposed to be written in such a way that safe code can't break it, which is a requirement clearly violated here. Therefore, the code is technically unsound, even if it happens not to have UB at the moment.
I know you're responding to the part about race conditions in particular, and not UB in general beyond the one example. However, given the wider audience, I again feel compelled to leave some notes of clarification:
It's the entire program that's single-threaded in this case; you cannot assume in the general case that you are single-threaded just because you are in main (by reasoning that "there can't be threads yet"). Nothing prevents a program from calling main() again from elsewhere.
But more critically, avoiding data races is not enough to avoid UB. And thus, being single-threaded is not enough to avoid UB. Aliasing a &mut is UB, and it's hard to be sure you never do that with static mut. Here are some examples you can run Miri against, which are UB even though there is no data race. [1]
Corollary: A non-unsafe function (including main) is never sound if it modifies a static mut without synchronization of some sort, given that a non-unsafe way to access said static mut is also present. (If one isn't present, you'll be using unsafe all over the place instead... but said functions could be technically sound.)
I'm aware the example doesn't do any of this, but wanted to highlight that data races aren't the only concern (and that you can't assume there are no data races in main). There are reasons RefCell requires a runtime check even though it's single-threaded, after all.
Using static mut requires verifying there are no overlapping accesses or lingering references across your entire program, which will contain a lot of unsafe; or being able to write a safe and sound abstraction around static mut. The former is increasingly hard as your program becomes longer and non-trivial, and just being single-threaded isn't sufficient (as demonstrated). It's also unidiomatic; in Rust, the goal is to contain unsafe actions behind safe APIs. However, even experts struggle with the latter (that is, writing a safe and sound abstraction around static mut), hence the probable deprecation.
So even though one can come up with perfectly sound examples as above, especially simple and small ones, it is still a dangerous foot gun. Use one of the alternatives presented instead.
In summary, I wish of most of this conversation didn't come up in this particular thread, and feel bad for the OP.
Click Miri under Tools; it is a tool that can catch many kinds of undefined behavior in Rust programs. ↩︎