Skip to main content

Sync

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.

Accessing critical section using mutex

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.

💡
Note: It’s not good practice to put everything inside the critical section. Putting everything inside a critical section may have a negative impact on execution time. If you write everything inside the critical section and a thread acquires the lock, then the rest of the threads will be put on hold, including those that had nothing to do with the synchronization. There might be situations in which a function takes a considerable amount of time to complete and doesn’t suffer any effects due to synchronization. In that case, the function will simply add up to the time of the complete program.

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) 
}