• Golang 的锁机制


    golang中的锁分为互斥锁、读写锁、原子锁即原子操作。在 Golang 里有专门的方法来实现锁,就是 sync 包,这个包有两个很重要的锁类型。一个叫 Mutex, 利用它可以实现互斥锁。一个叫 RWMutex,利用它可以实现读写锁。

    全局锁 sync.Mutex,是同一时刻某一资源只能上一个锁,此锁具有排他性,上锁后只能被此线程使用,直至解锁。加锁后即不能读也不能写。全局锁是互斥锁,即 sync.Mutex 是个互斥锁。

    读写锁 sync.RWMutex ,将使用者分为读者和写者两个概念,支持同时多个读者一起读共享资源,但写时只能有一个,并且在写时不可以读。理论上来说,sync.RWMutex 的 Lock() 也是个互斥锁。

    踩坑点

    将上面的结论展开一下,更清晰得说(为避免理解偏差宁可唠叨一些):

    • sync.Mutex 的锁是不可以嵌套使用的。
    • sync.RWMutex 的 mu.Lock() 是不可以嵌套的。
    • sync.RWMutex 的 mu.Lock() 中不可以嵌套 mu.RLock()。(这是个注意的地方)

    否则,会 panic fatal error: all goroutines are asleep - deadlock!

    1. var l sync.RWMutex
    2. func lockAndRead() { // 可读锁内使用可读锁
    3. l.RLock()
    4. defer l.RUnlock()
    5. l.RLock()
    6. defer l.RUnlock()
    7. }
    8. func main() {
    9. lockAndRead()
    10. time.Sleep(5 * time.Second)
    11. }

    而将 lockAndRead 换为以下三种函数均会造成 panic:

    1. func lockAndRead1() { // 全局锁内使用全局锁
    2. l.Lock()
    3. defer l.Unlock()
    4. l.Lock()
    5. defer l.Unlock()
    6. }
    7. func lockAndRead2() { // 全局锁内使用可读锁
    8. l.Lock()
    9. defer l.Unlock() // 由于 defer 是栈式执行,所以这两个锁是嵌套结构
    10. l.RLock()
    11. defer l.RUnlock()
    12. }
    13. func lockAndRead3() { // 可读锁内使用全局锁
    14. l.RLock()
    15. defer l.RUnlock()
    16. l.Lock()
    17. defer l.Unlock()
    18. }


    互斥锁 Mutex

    互斥锁有两个方法:加锁、解锁。

    一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)。使用Lock加锁后,不能再进行加锁,只有当对其进行Unlock解锁之后,才能对其加锁。这个很好理解。

    • 如果对一个未加锁的资源进行解锁,会引发panic异常。
    • 可以在一个goroutine中对一个资源加锁,而在另外一个goroutine中对该资源进行解锁。
    • 不要在持有锁的时候做 IO 操作。尽量只通过持有锁来保护 IO 操作需要的资源而不是 IO 操作本身
    1. func (m *Mutex) Lock()
    2. func (m *Mutex) Unlock()

    读写锁 RWMutex

    读写锁有四个方法:读的加锁、解锁,写的加锁、解锁。

    1. func (*RWMutex)Lock()
    2. func (*RWMutex)Unlock()
    3. func (*RWMutex)RLock()
    4. func (*RWMutex)RUnlock()

    RWMutex的使用主要事项

    • 1、读锁的时候无需等待读锁的结束
    • 2、读锁的时候要等待写锁的结束
    • 3、写锁的时候要等待读锁的结束
    • 4、写锁的时候要等待写锁的结束

    谨防锁拷贝

    1. type MyMutex struct {
    2. count int
    3. sync.Mutex
    4. }
    5. func main() {
    6. var mu MyMutex
    7. mu.Lock()
    8. var mu1 = mu
    9. mu.count++
    10. mu.Unlock()
    11. mu1.Lock()
    12. mu1.count++
    13. mu1.Unlock()
    14. fmt.Println(mu.count, mu1.count)
    15. }

    加锁后复制变量,会将锁的状态也复制,所以 mu1 其实是已经加锁状态,再加锁会死锁

    查看数据竞争

    加上 -race 参数验证数据竞争

    以下代码有什么问题,怎么解决?

    1. func main() {
    2. total, sum := 0, 0
    3. for i := 1; i <= 10; i++ {
    4. sum += i
    5. go func() {
    6. total += i
    7. }()
    8. }
    9. fmt.Printf("total:%d sum %d", total, sum)
    10. }

    该题的第二个考点:data race。因为存在多 goroutine 同时写 total 变量的问题,所以有数据竞争。可以加上 -race 参数验证

    1. go run -race main.go
    2. ==================
    3. WARNING: DATA RACE
    4. Read at 0x00c0001b4020 by goroutine 8:
    5. main.main.func1()
    6. /Users/xuxinhua/main.go:12 +0x57
    7. Previous write at 0x00c0001b4020 by main goroutine:
    8. main.main()
    9. /Users/xuxinhua/main.go:9 +0x10b
    10. Goroutine 8 (running) created at:
    11. main.main()
    12. /Users/xuxinhua/main.go:11 +0xe7
    13. ==================

    正确答案

    1. package main
    2. import (
    3. "sync/atomic"
    4. "sync"
    5. "fmt"
    6. )
    7. func main() {
    8. var wg sync.WaitGroup
    9. var total int64
    10. sum := 0
    11. for i := 1; i <= 10; i++ {
    12. wg.Add(1)
    13. sum += i
    14. go func(i int) {
    15. defer wg.Done()
    16. atomic.AddInt64(&total, int64(i))
    17. }(i)
    18. }
    19. wg.Wait()
    20. fmt.Printf("total:%d sum %d", total, sum)
    21. }

  • 相关阅读:
    JavaEE之HTTP协议 Ⅰ
    达梦8.0主备安装
    026-为什么要使用接口
    Android入门第10天-Android访问远程Spring Boot提供的Restful API接口
    软考高项八大绩效域及论文纲要
    哈希表5——两数之和
    自定义事件内容分发
    不同厂商IPC网页监控时延
    mysql操作实战案例
    【软件测试】接口——基本测试流程
  • 原文地址:https://blog.csdn.net/hudeyong926/article/details/126467118