Concurrent programming is hard, partly because concurrent anything is hard (even in meatspace).
Concurrent programming is hard, partly because concurrent anything is hard (even in meatspace).
The problem with what is described in the article is the race condition. If you hit the race on your one pass through this code, you're dead.
But there are other conditions whether you know you will repeatedly pass through the code, and the race doesn't matter.
So the language/programming model has abstractions that can be used successfully for the latter. This has the unfortunate side effect of them also being capable of being used to create bugs in the former.
If you want a real world example of this sort of thing, consider a single-reader/single-write circular buffer/queue. You don't need any sort of interlock between the read ptr/index and the write ptr/index for this sort of data structure because if they mis-ordered (ie. the writer has added more data but not yet updated the write ptr), the worst that can happen is that the reader thinks there is less data in the buffer to read than there actually is.
However, that analysis relies on the model that the reader and writer are periodically checking the data structure. If instead they are both doing a single write and a single read, this same design will lead to loss of data flow between them. To ensure this doesn't happen, you need a mutex that they both share.
So is the right thing to do to require any such shared data to be protected by a mutex? Well, if you're only goal is to prevent data races, then sure. But if you also care about e.g. realtime behavior, then absolutely not.
This is just one example. There are many more cases where in case A you don't want or need synchronization primitives and in case B you absolutely do.
E.g. when workers need to be stopped you close the channel sender, and receiver returns appropriate result to all wait calls.
E.g. yes, it is important to know the low-level details, but also the abstraction chosen is just not fit for the purpose. And it's partially fault of C++ because channels are not in the standard library.
It's the motivating example for languages like C++ in the first place (over assembly): type safety, easier reasoning about control flow, etc.
Note how the case here is very simple - waiting on a variable to change - but advice just boils down to "be more careful" instead of "here's the foolproof way to do this". You can only ask a programmer to watch out for so many things, and C++ has way too many of these things.