Mutex
What is mutex
?
In scenarios where exclusive access to a block of code is necessary for a single goroutine, a mutex serves as a locking and unlocking mechanism. By encapsulating the critical section within a mutex, we ensure that only one goroutine can execute the protected code at a time.
Mutexes are valuable for safeguarding caches, states, and registers from simultaneous access in concurrent environments. This ensures that the integrity of shared resources is maintained, preventing data corruption or race conditions.
As you can see above, only one goroutine can access the critical section at a time. A mutex guards the shared resource. It’s exclusive in nature. If a goroutine owns the mutex lock, then the same goroutine must unlock it and no other goroutine is allowed to take a lock on it until the lock is released.
Critical Section
In concurrent programming, when multiple processes have simultaneous access to shared resources, it can result in unexpected or erroneous behavior. The critical section is a designated code segment where processes can interact with shared variables. Within this critical section, processes must perform atomic actions, ensuring that only one process can execute at any given time.
Code Example
Create a unsafe-critical-section.go
in your current directory.
package main
import (
"fmt"
"runtime"
"sync"
)
func UnsafeCriticalSection() {
runtime.GOMAXPROCS(4)
value, x := 0, 5
var wg sync.WaitGroup
decrement := func() {
defer wg.Done()
value -= x
}
increment := func() {
defer wg.Done()
value += x
}
for i := 0; i < 200; i++ {
wg.Add(2)
go increment()
go decrement()
}
wg.Wait()
fmt.Println(value)
}
Now, in your main.go
file, call UnsafeCriticalSection()
method.
package main
func main() {
UnsafeCriticalSection()
}
Run go run .
to see the output.
-15
The code gives a different result with each execution. Why is this?
value -= x
or value += x
, both are not atomic operations. And hence variable value
is not concurrent safe. Overriding is taking place. The four hundred goroutines are racing to change the variable value.
Therefore, we need to protect this shared variable. And we can protect the variable by locking it i.e. making the variable thread-safe by either placing it inside the critical section or using an atomic action and this will return zero every time, because the operation will be done completely.
Thread Safe Code Example
Create a safe-critical-section.go
in your current directory.
package main
import (
"fmt"
"runtime"
"sync"
)
func SafeCriticalSection() {
runtime.GOMAXPROCS(4)
value, x := 0, 5
var wg sync.WaitGroup
var mu sync.Mutex
decrement := func() {
defer wg.Done()
mu.Lock() // locking the shared variable
value -= x
mu.Unlock() // unlocking the shared variable once the operation is complete
}
increment := func() {
defer wg.Done()
mu.Lock() // locking the shared variable
value += x
mu.Unlock() // unlocking the shared variable once the operation is complete
}
for i := 0; i < 200; i++ {
wg.Add(2)
go increment()
go decrement()
}
wg.Wait()
fmt.Println(value)
}
Now, in your main.go
file, call SafeCriticalSection()
method.
package main
func main() {
SafeCriticalSection()
}
Run go run .
to see the output.
0
The code above will always return zero as the result. Here, only one goroutine updates the value at a time. There’s no racing between different goroutines.
Test Yourself!
What is the output of the following code?
package main
import (
"fmt"
"sync"
)
var value = 0
func tempFunction(wg *sync.WaitGroup, mutex *sync.Mutex) {
mutex.Lock()
value = value + 1
mutex.Unlock()
wg.Done()
}
func main() {
var wg sync.WaitGroup
var mutex sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go tempFunction(&wg, &mutex)
}
w.Wait()
fmt.Println(value)
}