Mutexes
In the previous section, atomics were explained, and when atomicity was required was mentioned. When it comes to shared data, atomicity may not always be a solution or may be insufficient. For example, if you are working with a data type that is not atomically supported, or if your algorithm needs safe execution, atomic types will not be enough to solve your problem.
For example:
use "std/sync"
let mut n = 0
async fn addToN(mut wg: &sync::WaitGroup) {
n++
wg.Done()
}
async fn main() {
mut wg := sync::WaitGroup.New()
const Total = 1_000_000
mut j := 0
for j < Total; j++ {
wg.Add(1)
co addToN(wg)
}
wg.Wait().await
println(n)
}In the above code, not only is the value of the n variables is manipulated, but some functions are also called. Atomic types are not a good approach in such cases.
To solve this problem, we can use a mutex. Mutexes are locking mechanisms that allow you to do this. They can only be locked by a single coroutine at a time. If a locking attempt is made by a different coroutine when they are locked, the execution of the coroutine will be stopped until the locking coroutine releases the lock.
For example:
use "std/sync"
let mut n = 0
let mtx = new(sync::Mutex)
async fn addToN(mut wg: &sync::WaitGroup) {
mtx.Lock().await
n++
mtx.Unlock()
wg.Done()
}
async fn main() {
mut wg := sync::WaitGroup.New()
const Total = 1_000_000
mut j := 0
for j < Total; j++ {
wg.Add(1)
co addToN(wg)
}
wg.Wait().await
println(n)
}In the code above, synchronization between coroutines is achieved by using a mutex. Thanks to Mutex, only one coroutine can continue executing the relevant function at a time, and the others are waiting to take over the lock. This means that another coroutine cannot continue execution until one coroutine has completely completed its execution. In this way, a race condition is prevented.