Use mutex when you need to coordinate multiple values, or the values are larger than the maximum atomic size, or the critical sections are more complex than a simple atomic swap/incr/whatever.
Both are portable, this is not an issue.
If your value is larger than atomic size on your platform, you obviously need a mutex. There are also special problems, where you have read-heavy workload, and then some more complex write operation and you have concurrent writers, it might actually make sense to use both mutex and atomic. Atomic load on the read path, mutex + atomic write on the write path. Generally, under really heavy contention, mutexes do better. It’s counter-intuitive, because the promise of lock free is that it always advances, but CPUs still need to lock cachelines, etc.
You already should use atomic.
Atomic is not an I/O concern. Mutex is in std.Io because it has to park the current task while it waits.
Just adding onto @lalinsky’s great response, look into a concept called “false sharing”.
Since you are dealing with really small shared values, it may be that you have the thread-1 and thread-2 specific data next to each other in memory. I know you didn’t really ask for performance differences, but if you are interested this could be much more important to speed of your application than Mutex vs Atomic choice.
I agree that atomics are the correct choice contextually. @lalinsky answer sums it up pretty well but I’d also add onto it that even when an atomic’s size would be enough there’s still a situation where you would want to use a Mutex.
Atomics by themselves don’t guarantee “mutual exclusivity” in the same way mutexes do. They guarantee order of operations.
So, as you say, if you need to protect this value within a block and you want to make sure no changes happen while you are doing some operations, you should use a mutex.
A thing to be aware of is that the std library actually has two different types of Mutex:
std.Io.Mutex
std.atomic.Mutex
They both have the same function; prevent two threads from simultaneously entering a ‘critical section’ of code. std.atomic.Mutex is much simpler though, and doesn’t require an Io parameter.
Why then do we use std.Io.Mutex?
The problem is that your program runs under an operating system, and the OS can - at any moment - pause a thread and use the CPU to do something else for a while. If that happens in the middle of a critical section, the mutex will remain locked until the OS decides its time to run that thread again, which could take a very long time.
What do the other threads do in the meantime? If you’re using std.Atomic.Mutex, they will continue making futile attempts to lock the mutex, completely wasting CPU time until the OS pauses them as well. To make matters worse, there is no guarantee that the thread holding the mutex will start running before the threads that are waiting for it!
std.Io.Mutex solves this problem by taking an Io parameter. It uses it to tell the OS that it can’t proceed, and more importantly, why it can’t proceed, by passing pointer to the mutex it’s waiting for. The OS immediately stops running the thread, and doesn’t bother running it again until the thread that holds the mutex unlocks it.
There are situations in which std.atomic.Mutex is the better choice, but these are rare. If in doubt, use std.Io.Mutex.
I honestly don’t know if I should really send the following and do the “uhm well akshually ”… but here I am. I just find that answer so … I can’t really describe it. This is by no means meant to be mean. I also don’t know if it’s just a language thing.
Atomic comes from indivisible. And this at least to me is much better at explaining it.
Everything on a computer is.
Atomics aren’t “single”. They are, at least for all the microarchitectures I’ve used, implemented in multiple uops(as are most other instructions). See here for amd64.
The second point about the “mid point”/“seem” is more correct. Another cpu can’t read a torn state. So if one cpu changes a value from 0x0000000 to 0xffffffff another one can’t read 0x0000ffff. This is the meaning of indivisible. It is, to me, somewhat similar to a database transaction but just on a single word.
Operating system integration for mutexes is mostly to avoid busy wait loops and maybe don’t schedule lock holder off the cpu to keep the critical section short.
You don’t have to. Though generally using a mutex is a good starting point. There are various techniques that can be much faster and efficient than a mutex depending on how and when the data is changed; the read vs. write ratio; the length of the critical section…
Just as a fun fact, swap(true, .acl_rql) is the faster option for a spinlock. Was recently watching this video on atomics, really interesting stuff:
(Disclaimer: this is super advanced, don’t use spinlocks unless you really know what you are doing. If you are not sure, just use std.Io.Mutex, your code will be safe and fast enough.)