Let’s start with a couple of concepts

A critical region

In concurrent programming, if a part of a program can be accessed or modified concurrently, that part of the program needs to be protected to avoid unexpected results caused by concurrent access. The protected part of the program is called a critical section. A critical section is a shared resource, or a group of shared resources as a whole, such as access to a database, operations on a shared data structure, use of an I/O device, calls to connections in a connection pool, and so on.

When a critical section is accessed synchronously by multiple threads, an access or operation error can occur, so a mutex is needed to define that a critical section can only be held by one thread at a time.

Synchronization primitives

Synchronization primitives are a basic data junction to solve concurrency problems. In Golang, Mutex, RWMutex, WaitGroup, Cond, Channel, and so on are synchronization primitives

Synchronization primitives are typically used in the following scenarios:

  • Share resources. Concurrent reads and writes lead to data races. Use Mutex, RWMutex
  • Task scheduling. The goroutine is required to execute in a regular manner, and the goroutine has a waiting or dependent order, which can be implemented using a WaitGroup or Channel.
  • Message passing. Communication, as well as thread-safe communication between different Goroutines, can be implemented using channels.

Basic usage of Mutex

Mutex may operate in two modes: normal mode and starvation mode.

  • Normal mode: All coroutines queue in FIFO mode. Awakened coroutines also need to compete for the lock. New coroutines have an advantage because they are already running on the CPU and are easier to grab the lock.
  • Hunger mode: The mutex is handed directly from the unlocked coroutine to the waiting person at the head of the queue. The new contender cannot acquire the lock directly and will obediently join the end of the queue without attempting spin.

Mutex methods are composed of Lock() and Unlonk() methods. Once a Mutex is locked, it cannot be locked again until Unlock() is unlocked. Mutex is suitable for uncertain read/write scenarios and only one read/write is allowed.

Atomicity, which locks a mutex as an atomic operation, ensures that if one coroutine locks a mutex, no other coroutine can successfully lock the mutex at the same time. Uniqueness: If one coroutine locks a mutex, no other coroutine can lock the mutex until it is unlocked. A mutex can only be locked once, and cannot be locked if it is locked again before unlocking. If unlocked before unlocked, “Panic: sync: unlock of unlocked Mutex” is displayed. The mutex has no conflict. If there is a conflict, spin first and then get the lock after a short spin. If there is no result of spin, the coroutine is told to wait by signal.

Let’s start with an example

package main

import (
	"fmt"
	"sync"
)

func main() {
	var count = 0
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				count++
			}
		}()
	}
	wg.Wait()
	fmt.Println(count)
}
Copy the code

It should be known that this output will not be 10000. Since count++ is not an atomic operation, concurrency issues will occur.

Go officially provides a tool to detect problems with concurrent access to shared resources: Race Detector, which can help us automatically detect problems with data races.

Adding the race parameter when compiling, testing, or running Go code makes it possible to detect concurrency problems.

go run -race main.go
Copy the code

Mutex comes into play when there is a Data race. Before and after count++ we lock unlock to get the desired result.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	var count = 0
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				mu.Lock()
				count++
				mu.Lock()
			}
		}()
	}
	wg.Wait()
	fmt.Println(count)
}

Copy the code

There are four common error scenarios

  • Lock/Unlock does not come in pairs

  • reentrant

  • A deadlock

  • Copy Used synchronization primitives of Mutex Package sync cannot be copied after use. A Mutex is a stateful object whose state field records the state of the lock. If you copy a locked Mutex to a new variable, the newly initialized variable will be locked.

    Workaround: At runtime, Go has a deadlock checking mechanism (checkdead() method), which can find deadlocked goroutine. Deadlock detection using Go Vet

    go vet main.go
    Copy the code