1
头图

Hello everyone, today I will sort out the Go language concurrency knowledge content and share it with you. Please advise, thank you.

This "Go Language Concurrency Knowledge" is divided into three chapters, and this article is the third chapter.

Contents of this chapter

  • Basic synchronization primitives
  • Common lock types
  • Extended content

Basic synchronization primitives

The Go language provides some basic primitives for synchronization in the sync package, including the common mutex Mutex and the read-write mutex RWMutex and Once , WaitGroup . The main function of these basic primitives is to provide relatively basic synchronization functions. This time, only Mutex will be introduced, and the remaining other primitives will be used in subsequent concurrency chapters.

qPYtXT.jpg

What is Mutex

Mutex is the mutex of the golang standard library, which is mainly used to deal with the access conflict of shared resources in concurrent scenarios.

Mutex sync包中, state semastate Indicates the current state of the mutex lock, and sema the semaphore that is really used to control the lock state, these two structures that only occupy 8 bytes of space together represent the mutex in the Go language. Lock.

 type Mutex struct {
    state int32
    sema  uint32
}

The role of a mutex is to synchronize access to shared resources. The name mutex comes from the concept of mutual exclusion, which is used to create a critical section on the code to ensure that only one goroutine can execute the critical section code at the same time.

 package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    counter int
    wg sync.WaitGroup
    mutex sync.Mutex // 定义代码临界区
)

func main() {
    wg.Add(2)
    go incCounter()
    go incCounter()
    wg.Wait()
    fmt.Println("counter:", counter)
}

func incCounter() {
    defer wg.Done()
    for count := 0; count < 2; count++ {
        mutex.Lock() // 临界区, 同一时刻只允许一个 goroutine 进入
        {
            value := counter
            runtime.Gosched() // goroutine退出,返回队列
            value++
            counter = value
        }
        mutex.Unlock() // 释放锁
    }
}

Lock() and Unlock() are protected in the critical section defined by the function call. The curly braces are used only to make the critical section look clearer and are not required. Only one goroutine can enter the critical section at the same time, until the Unlock() function is called, other goroutines can enter the critical section.

Several states of Mutex

  • mutexLocked — indicates the locked state of the mutex;
  • mutexWoken — indicates a wake-up from normal mode;
  • mutexStarving — the current mutex is starved;
  • waitersCount — the number of Goroutines currently waiting on the mutex;

normal mode and starvation mode

sync.Mutex There are two modes - normal mode and starvation mode.

In normal mode, lock waiters acquire locks in a first-in, first-out order. However, when the newly awakened Goroutine competes with the newly created Goroutine, there is a high probability that the lock will not be acquired. In order to reduce the occurrence of this situation, once the Goroutine does not acquire the lock for more than 1ms, it will switch the current mutex to starvation mode. , to prevent some Goroutines from being "starved".

Starvation mode is an optimization introduced in Go language 1.9 by submitting sync: make Mutex more fair . The purpose of introduction is to ensure the fairness of mutex locks.

In starvation mode, the mutex is directly handed over to the goroutine at the front of the waiting queue. New goroutines in this state cannot acquire locks and do not spin, they just wait at the end of the queue. If a goroutine acquires the mutex and it is at the end of the queue or it waits less than 1ms, the current mutex switches back to normal mode.

Common lock types

Deadlock, Livelock and Starvation

These three lock modes have been briefly explained in the article [Concurrency knowledge based on Golang (1)](), and a supplement is made for the starvation mode above.

Deadlock, as the most common lock, here is a supplement.

Deadlock can be understood as the resources for completing a task are occupied by two (or more) different coroutines respectively, causing them all to be in a waiting state and cannot be completed. In this case, the program will never resume without external intervention.

 // 死锁案例
package main

import (
    "fmt"
    "sync"
    "time"
)
type value struct {
    mu sync.Mutex
    value int
}

var wg sync.WaitGroup

func main() {
    printSum := func(v1, v2 *value) {
        defer wg.Done()
        v1.mu.Lock() // 加锁
        defer v1.mu.Unlock() // 释放锁

        time.Sleep(1 * time.Second)
        v2.mu.Lock()
        defer v2.mu.Unlock()
        fmt.Printf("sum=%v\n", v1.value+v2.value)
    }

    var a, b value
    wg.Add(2)
    go printSum(&a, &b) // 协程1
    go printSum(&b, &a) // 协程2
    wg.Wait()
}

output

 fatal error: all goroutines are asleep - deadlock!

Three actions of deadlock

  1. Attempt to access locked section
  2. Attempt to release the lock by calling the defer keyword
  3. Add sleep time to cause deadlock

qC0Q61.png

Essentially, we create two gears that don't work together: our first print-sum call a locks, then tries to lock b, but at the same time, our second print-sum call locks b and tries to lock a . Both goroutines are waiting for each other indefinitely.

spin lock

introduce

Spin lock means that when a thread acquires the lock, if the lock has been acquired by other threads, the thread will wait in a loop, and then continuously judge whether it can be successfully acquired, and will not exit the loop until the lock is acquired.

The thread that acquires the lock is always active, but does not perform any valid tasks. Using this kind of lock will cause busy-waiting .

It is a lock mechanism proposed to protect shared resources. In fact, spin locks are similar to mutex locks, both of which are used to solve the mutual exclusion of a resource. Whether it is a mutual exclusion lock or a spin lock, at most one holder can obtain the lock at any time, that is to say, at most one execution unit can acquire the lock at any time. But the two have slightly different scheduling mechanisms. For a mutex, if the resource is already occupied, the resource applicant can only enter the sleep state. But the spin lock does not cause the caller to sleep. If the spin lock is already held by another execution unit, the caller keeps looping there to see if the spin lock holder has released the lock, the word "spin" That's how it got its name.

Spinlocks and Mutexes
  • Both spin locks and mutex locks are mechanisms for protecting resource sharing.
  • Whether it is a spin lock or a mutex, there can only be at most one holder at any time.
  • The thread that acquires the mutex lock will enter the sleep state if the lock is already occupied; the thread that acquires the spin lock will not sleep, but will wait for the lock to be released in a loop.
Summarize
  • Spin lock: When a thread acquires a lock, if the lock is held by another thread, the current thread will wait in a loop until the lock is acquired.
  • During spinlock waiting, the state of the thread does not change, the thread is always in user mode and is active.
  • Holding a spin lock for too long can cause other threads waiting to acquire the lock to run out of CPU.
  • Spinlocks by themselves do not guarantee fairness, nor do they guarantee reentrancy.
  • Based on spin locks, locks with fairness and reentrancy can be implemented.

Read-write lock

Read-write locks are mutex locks for read-write operations. The biggest difference between it and ordinary mutex is that it can be locked and unlocked for read and write operations respectively. Read-write locks follow different access control rules. The multiple write operations under the control of the read-write lock are mutually exclusive, and the write operations and read operations are also mutually exclusive.

However, there is no mutual exclusion relationship between multiple read operations. Under such a mutual exclusion strategy, read-write locks can complete access control to shared resources while greatly reducing the performance loss caused by the use of locks.

Read-write locks in Go are represented by the structure type sync.RWMutex . As with mutexes, a zero value of type sync.RWMutex is already an available read-write lock instance.

 // 类型方法集
func (*RWMutex) Lock()
func (*RWMutex) Unlock()
func (*RWMutex) RLock()
func (*RWMutex) RUnlock()

Extended content

unfair lock

Unfair locks can be seen as a less serious form of starvation, when some threads are competing for the same lock, some of them can acquire the lock most of the time, while others suffer unfair treatment. This can happen on machines with shared caches or NUMA memory, if CPU 0 releases a lock that other CPUs want to acquire, because CPU 0 shares internal connections with CPU 1, CPU 1 is less than CPU 2 to 7 It is easier to grab the lock.

Vice versa, if CPU 0 starts fighting for the lock again after a while, then it is easier for CPU 0 to acquire the lock when CPU 1 releases the lock, causing the lock to bypass CPUs 2 to 7 and only switch between CPUs 0 and 1. hand.

inefficient lock

Locks are implemented by atomic operations and memory barriers, and often result in cache misses. These instructions are expensive, roughly two orders of magnitude more expensive than simple instructions. This can be a serious problem with locks, and if you use a lock to protect an instruction, you're probably incurring overhead a hundred times faster. For the same code, even assuming perfect scaling, it would take 100 CPUs to keep up with a CPU executing the unlocked version.

However, once the lock is held, the holder can access the code protected by the lock undisturbed. Acquiring a lock can be expensive, but once held, especially for large critical sections, the CPU's cache is an efficient performance accelerator.

Technical articles are continuously updated, please pay more attention~~

Search WeChat public account and follow me [The Gunslinger of Maoer Mountain]


References

"Go Language Design and Implementation" book "Concurrency in Go" book "Go Concurrent Programming Practice" book "Go Language Practice" book "Go Concurrent Programming Practice Class" by Mr. Chao Yuepan (Bird's Nest)
"In-depth understanding of parallel programming" book


帽儿山的枪手
71 声望18 粉丝