go sync包

Official documentation: https://pkg.go.dev/sync

critical section

A critical section refers to a section of code that contains shared data that may be accessed
or modified by multiple threads. The existence of the critical section is to ensure that when a thread is executed in the critical section, no other thread can be allowed to execute in the critical section.
Each critical section has a corresponding entry section and exit section, which can be represented as shown in Figure 2.3.
Insert image description here
Suppose there are two threads A and B executing the same piece of code, then at most one thread can execute the code in the critical section at any time. That is, if thread A is executing in the critical section, thread B can only wait in the entry section. Only when thread A finishes executing the code in the critical section and exits the critical section, thread B, which was originally in the waiting state, can continue to execute downwards and enter the critical section.

If there is only one main thread, the input result is normal

package main

import "fmt"

var x = 0

func main() {
    
    
    for i := 0; i < 10000; i++ {
    
    
        x = x + 1
    }
    for i := 0; i < 10000; i++ {
    
    
        x = x + 1
    }
    fmt.Printf("x value is %d\n", x)
}

Output result: x value is 20000, as expected

Change to multi-coroutine access, the code is as follows

package main

import (
    "fmt"
    "sync"
)

var x = 0
var wg sync.WaitGroup

func add() {
    
    
    for i := 0; i < 10000; i++ {
    
    
        x = x + 1
    }
    wg.Done()
}
func main() {
    
    
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Printf("x value is %d\n", x)
}

Input result: x value is 12718 (different each time it is executed), which does not meet expectations.
Cause analysis:
x = x + 1

  1. First get the value of x
  2. Calculate x + 1
  3. Assign the calculation result of step 2 to x

Assume that the x values ​​obtained by coroutines a and b are both 99. When coroutine a proceeds to step 2, coroutine b proceeds to step 3. Coroutine b gives the value 100 in step 2 to x, and coroutine a starts again. Give the value 100 in step 2 to x, and overwrite the value of the b coroutine. As a result, the value of x, 99, after adding 1 twice, is 100 instead of the expected 101.

mutex lock

Mutual exclusion lock (English: Mutual exclusion, abbreviated as Mutex) is a mechanism used in multi-threaded programming to prevent two threads from reading and writing the same public resource (such as global variables) at the same time. This goal is achieved by slicing the code into critical sections.
sync.Mutex defines two methods: Lock and Unlock . All code between Lock and Unlock can only be executed by one Go coroutine, so race conditions can be avoided.
The improved procedure is as follows

package main

import (
    "fmt"
    "sync"
)

var x = 0
var mutex sync.Mutex
var wg sync.WaitGroup

func add() {
    
    
    for i := 0; i < 10000; i++ {
    
    
        mutex.Lock()
        x = x + 1
        mutex.Unlock()
    }
    wg.Done()
}
func main() {
    
    
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Printf("x value is %d\n", x)
}

The output result each time: x value is 20000, in line with expectations.

read-write lock

Read operations can be reentrant concurrently, and write operations are mutually exclusive . This means that multiple threads can read data at the same time, but when writing data, they need to obtain an exclusive lock. When a writer writes data, other writers or readers need to wait until the writer completes the write operation . Read-write locks use the RWMutex type in the sync package in the Go language.

Read-write lock priority strategy

Read-write locks can have different operating mode priorities:

  • Read operation priority lock: Provides maximum concurrency, but when lock competition is fierce, it may lead to starvation of write operations. This is because as long as there is a reading thread holding the lock, the writing thread cannot get the lock. Multiple readers can get the lock at once, which means that a writer may be waiting for the lock while new readers can get the lock. In extreme cases, the writer thread may wait for the lock until all readers who initially obtained the lock release the lock. Readers can have weak priority, as mentioned above, or strong priority, that is, as long as the writer releases the lock, any waiting readers can always get it first.
  • Write operation priority lock: If there are writers in the queue waiting for the lock, any new readers are prevented from taking the lock, thereby avoiding the problem of write operation starvation. Once all read operations that have been started are completed, the waiting write operation immediately acquires the lock. Compared with the read operation priority lock, the disadvantage of the write operation priority lock is that the concurrency is low when the writer exists. The internal implementation requires two mutex locks.
  • Unspecified priority lock: does not provide any read/write priority guarantee.

Go read-write locks (sync.RWMutex) do not cause starvation of write operations. Read-write locks allow multiple goroutines to read shared resources at the same time, but only one goroutine can perform write operations. When a write operation is waiting, the read operation will be blocked until the write operation is completed. This ensures that write operations are not delayed indefinitely, thus avoiding the problem of write starvation. At the same time, the read-write lock also supports priority inversion, that is, when a write operation is waiting, new read operations will also be blocked to ensure that the write operation is executed as soon as possible.

  • RWMutex can only be held by any number of readers at a time, or by a single writer.
  • Lock/Unlock: Method called during write operation . If the lock is already held by a reader or writer, the Lock method will block until the lock can be acquired; Unlock is the paired method of releasing the lock.
  • RLock/RUnlock: Method called during read operation . If the lock is already held by the writer, the RLock method will block until the lock can be obtained, otherwise it will return directly; and RUnlock is the reader's method to release the lock.
  • RLocker: The function of this method is to return an object of the Locker interface for the read operation. Its Lock method will call the RLock method of RWMutex, and its Unlock method will call the RUnlock method of RWMutex.
    The advantage of read-write locks is when there is more reading and less writing, for example:
// 使用互斥锁
package main

import (
    "fmt"
    "sync"
    "time"
)

var x = 10
var wg sync.WaitGroup

var mutex sync.Mutex

func write() {
    
    
    mutex.Lock()
    time.Sleep(1 * time.Millisecond) // 模拟写耗时1毫秒
    x = x + 1
    mutex.Unlock()
    wg.Done()

}
func read() {
    
    
    mutex.Lock()
    time.Sleep(time.Millisecond) // 模拟读耗时1毫秒
    mutex.Unlock()
    wg.Done()

}
func main() {
    
    
    // 统计开始时间
    time1 := time.Now()
    // 开10个协程写
    for i := 0; i < 10; i++ {
    
    
        wg.Add(1)
        go write()
    }
    // 开1000个协程读
    for i := 0; i < 1000; i++ {
    
    
        wg.Add(1)
        go read()
    }

    wg.Wait()
    fmt.Println("x最终值为:", x)
    // 统计结束时间
    time2 := time.Now()
    fmt.Printf("总共耗时:%v\n", time2.Sub(time1)) // 结束时间-开始时间
}

The input result is as follows:
The final value of x is: 20
Total time taken: 15.7080693s

// 使用读写锁
package main

import (
    "fmt"
    "sync"
    "time"
)

var x = 10
var wg sync.WaitGroup

var rwMutex sync.RWMutex

func write() {
    
    
    rwMutex.Lock()                   // 写锁都用Lock
    time.Sleep(1 * time.Millisecond) // 模拟写耗时1毫秒
    x = x + 1
    rwMutex.Unlock()
    wg.Done()

}
func read() {
    
    
    rwMutex.RLock()              // 读锁用RLock
    time.Sleep(time.Millisecond) // 模拟读耗时1毫秒
    rwMutex.RUnlock()
    wg.Done()

}

func main() {
    
    
    // 统计开始时间
    time1 := time.Now()
    // 开10个协程写
    for i := 0; i < 10; i++ {
    
    
        wg.Add(1)
        go write()
    }
    // 开1000个协程读
    for i := 0; i < 1000; i++ {
    
    
        wg.Add(1)
        go read()
    }

    wg.Wait()
    fmt.Println("x最终值为:", x)
    // 统计结束时间
    time2 := time.Now()
    fmt.Printf("总共耗时:%v\n", time2.Sub(time1)) // 结束时间-开始时间
}

The output result is as follows:
The final value of x is: 20
Total time taken: 155.8851ms

sync.WaitGroup

Please refer to: https://blog.csdn.net/weixin_37909391/article/details/130853859

sync.Once

sync.Once has only one method, the signature is as follows

func (o *Once) Do(f func())

Do calls function f if and only if Do is called for the first time for an instance of Once.
If once.Do(f) is called multiple times, only the first call will call f, even if f has a different value in each call. Each function to be executed requires a new Once instance.

package main

import (
    "fmt"
    "sync"
)

func main() {
    
    
    var once sync.Once
    onceBody := func() {
    
    
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
    
    
        go func() {
    
    
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
    
    
        <-done
    }
}

Result: Only once

sync.Map

Simultaneous reading (without writing operations) of the same map in multiple goroutines will not cause data competition and concurrent access problems. Therefore, in the case of only read operations, the original map can be used directly without worrying about concurrent access.

method illustrate
func (m *Map) CompareAndDelete(key, old any) (deleted bool) Use sparingly, omit
func (m *Map) CompareAndSwap(key, old, new any) bool Use sparingly, omit
func (m *Map) Delete(key any) Delete deletes the value of the key.
func (m *Map) Load(key any) (value any, ok bool) Load returns the key value stored in the map, or nil if the value does not exist. The ok result indicates whether the value was found in the map.
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) LoadAndDelete deletes the value of a key, returning the previous value (if any). The loaded results report whether the key exists.
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) LoadOrStore Returns the existing value of the key if it exists. Otherwise, it stores and returns the given value. The load result is true if the value has been loaded, false if it has been stored.
func (m *Map) Range(f func(key, value any) bool) Range calls f in turn for each key and value present in the map. If f returns false, range stops iterating.
func (m *Map) Store(key, value any) Store sets the value of the key.
func (m *Map) Swap(key, value any) (previous any, loaded bool) Use sparingly, omit

It should be noted that because sync.Map uses some tricks internally to achieve concurrency safety, some of its methods may be slower than ordinary map operations. In scenarios with high performance requirements, you can consider using other concurrency-safe data structures, such as sync.Pool, atomic.Value, etc.

package main

import (
    "fmt"
    "sync"
)

var m = make(map[int]int)

func main() {
    
    
    var wg sync.WaitGroup
    for i := 0; i < 20; i++ {
    
    
        wg.Add(1)
        go func(n int) {
    
    
            m[n] = n * 10
            fmt.Printf("key为:%d,value为:%d\n", n, m[n])
            wg.Done()
        }(i)
    }
    wg.Wait()
    // panic:fatal error: concurrent map writes
}

package main

import (
    "fmt"
    "sync"
)

var m = sync.Map{
    
    }

func main() {
    
    
    var wg sync.WaitGroup
    for i := 0; i < 20; i++ {
    
    
        wg.Add(1)
        go func(n int) {
    
    
            m.Store(n, n*10)
            res, _ := m.Load(n)
            fmt.Printf("key为:%d,value为:%d\n", n, res)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

The results are as follows:
key is: 0, value is: 0
key is: 19, value is: 190
key is: 5, value is: 50
key is: 6, value is: 60
key is: 7, value is: 70
key is : 4, value is: 40
key is: 8, value is: 80
key is: 9, value is: 90
key is: 10, value is: 100 key is:
11, value is: 110
key is: 3, value is :30
key is: 12, value is: 120
key is: 15, value is: 150
key is: 13, value is: 130
key is: 18, value is: 180
key is: 14, value is: 140
key is: 17, value is: 170
key is: 1, value is: 10
key is: 16, value is: 160
key is: 2, value is: 20

Guess you like

Origin blog.csdn.net/weixin_37909391/article/details/130863240