美文网首页
GO基础学习(13)锁

GO基础学习(13)锁

作者: 温岭夹糕 | 来源:发表于2023-05-07 22:51 被阅读0次

写在开头

非原创,知识搬运工/整合工,仅用于自己学习,本文是对锁的学习

往期回顾

  • 基础章节
  1. 基本数据类型
  2. slice/map/array
  3. 结构体
  4. 接口
  5. nil
  6. 函数
  • GMP章节
  1. GO调度器
  2. GMP介绍
  3. 调度器初始化
  4. 循环调度
  • 并发编程
  1. 内存重排
  2. 锁的源码阅读

带着问题去阅读

  1. 介绍一下抢锁流程
  2. 什么是自旋锁,优缺点
  3. Go的锁是公平的吗
  4. 锁的两种模式,切换时机
  5. 新的G更有机会拿到锁吗
  6. 互斥锁和读写锁区别
  7. 读写锁如何做到读锁不互斥,读写互斥
  8. Mutex如何挂起协程G,如何唤醒
  9. GO的锁可以重复上锁或解锁吗
  10. 什么是double-check
  11. 什么是死锁

1.源码复习

锁的源码阅读
我们知道,在多线程的情况下,很容易发生数据竞争和内存重排等现象,这给我们的程序带来很多的不确定性,加锁能很好的防止这种现象。GO的锁分为互斥锁和读写锁,读写锁又在互斥锁的基础上使用装饰器模式进行包装。

1.1互斥锁

其中互斥锁的实现原理为 image.png
  1. 本质上是一个协程G给一把锁(Mutex结构体实例)的state状态字段设置为(利用atomic.compareAndSwapInt32函数)上锁mutexLocked(mutexLocked=1),改变成功则返回,失败说明该锁正被其他协程G占用,开始抢锁lockSlow(锁上并没有记录是哪个协程给他上的锁)
  2. 先尝试进行自旋获取锁(自旋就是当一个线程获取锁,该锁已经被占用,则等待一段时间后再次尝试,如此循环,CPU层面实现的),自旋锁会一直占用CPU核(线程不做切换),当自旋次数满后还没获取锁则进入下一步

自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。

  1. 此时不再主动获取锁,进入等待队列并休眠,等待被信号唤醒再去抢锁
  2. 还抢不到就重新开始自旋再来一遍(实际上自旋次数已经满了,不需要再自旋了,只是再次进入等待队列,这里我们可以看出)
  3. 长时间等待(1ms)则进入饥饿模式,更有机会被抢到锁,饥饿模式的存在说明了GO的锁是不公平的

这里我们不难看出新来的G更容易抢到锁,因为自旋的机制,旧的G即使从队列中被唤醒,因为无法自旋,若不是饥饿模式需要重新休眠,更难抢到

1.2读写锁

读写锁RWMutex是写锁优先,即读锁可以重复加,但当读写锁之间互斥 image.png

从结构体成员上可以看出是对mutex的包装

type RWMutex struct {
    w           Mutex        // held if there are pending writers
    writerSem   uint32       // semaphore for writers to wait for completing readers
    readerSem   uint32       // semaphore for readers to wait for completing writers
    readerCount atomic.Int32 // number of pending readers
    readerWait  atomic.Int32 // number of departing readers
}

Q:如何实现读锁重复添加?
A:读锁加锁并不是操作内部互斥锁,是执行readerCount+1的操作
Q:如何实现加写锁的时候不能加读锁
A:RWMutex添加写锁时会将readerCount设置为负数,读锁加锁时发现为负数则陷入休眠,等待被唤醒(写锁完成后用循环唤醒全部的读锁)
Q:如何实现加读锁时无法加写锁
A:readerWait大于0表示还有读锁存在,写锁陷入休眠,只有当readerWait==0,写锁才能开始加锁

1.3atomic

另一个同步原语atomic实现都是依赖底层汇编

2.实验-锁的错误使用

2.1同协程重复加互斥锁

type MyData struct {
    id int
    mu sync.Mutex
}

func TestLockAgain(t *testing.T) {
    data := MyData{id: 1}
    data.mu.Lock()
    data.mu.Lock()
    data.mu.Unlock()
    data.mu.Unlock()
}

//panic: test timed out after 30s

运行超时引发的panic,这个就不分析了

2.2重复解锁

func TestUnLockAgain(t *testing.T) {
    data := MyData{id: 0}
    data.mu.Lock()
    defer data.mu.Unlock()
    data.mu.Unlock()
}
//fatal error: sync: unlock of unlocked mutex

造成原因通过阅读源码unlock的unlockslow函数就知道了,unlock尝试对Mutex的state字段-1,当值不为0则进入unlockslow,由unlockslow判断
上面的错误由下面代码引发

//上面情况new = -1  mutexLocked =1
func (m *Mutex) unlockSlow(new int32) {
    if (new+mutexLocked)&mutexLocked == 0 {
        fatal("sync: unlock of unlocked mutex")
    }

2.3复制锁引发的错误

使用复制锁引发的错误归根揭底是无法确定这把锁的状态,因为锁上不保存上锁协程的信息,意味着G1上锁了,G2却能解锁,下面是给复制锁解锁的例子

func TestCopyMutex(t *testing.T) {
    data := MyData{id: 0}
    go func() {
        data.mu.Lock()
        defer data.mu.Unlock()
        time.Sleep(time.Hour)
    }()
    time.Sleep(time.Second)
    go func(mydata MyData) {
        mydata.mu.Unlock()
        mydata.id = 2
        t.Log(mydata.id)
        time.Sleep(time.Hour)
    }(data)
    time.Sleep(time.Second * 5)
}

此时代码没问题,锁被复制前是上锁的状态,若去掉第一个G中的defer关键词,则锁被复制时是解锁状态,重复解锁引发panic
所以上锁解锁的原则是在同一个函数体或大括号中

2.4死锁引发的panic

源码阅读那一章举过例子

func TestDeadlock(t *testing.T) {
    d1 := MyData{id: 1}
    d2 := MyData{id: 2}
    go func() {
        d1.mu.Lock()
        defer d1.mu.Unlock()
        time.Sleep(time.Second)
        t.Log("g1-d1.id:", d1.id)
        d2.mu.Lock()
        t.Log("g1-d2.id:", d2.id)
        d2.mu.Unlock()
    }()
    go func() {
        d2.mu.Lock()
        defer d2.mu.Unlock()
        time.Sleep(time.Second)
        t.Log("g2-d2.id:", d2.id)
        d1.mu.Lock()
        t.Log("g2-d1.id:", d1.id)
        d1.mu.Unlock()
    }()
    var ch = make(chan int)
    <-ch
}

G1和G2互相需要对面的资源和拥有的锁,都在互相等待对面释放锁,结果一起休眠,解决办法就是缩小锁住的范围或使用atomic

    go func() {
        d1.mu.Lock()
        time.Sleep(time.Second)
        t.Log("g1-d1.id:", d1.id)
        d1.mu.Unlock() //使用完就释放,不再锁住整个函数
        d2.mu.Lock()
        t.Log("g1-d2.id:", d2.id)
        d2.mu.Unlock()
    }()

2.5双重检查-double-check

解决一个问题就会引发新的问题,如上,锁的粒度太细,在多线程下也会产生奇怪的结果

func TestDoubleCheck(t *testing.T){
    type safeData struct{
        values map[int]int
        lock sync.RWMutex
    }

    data:=safeData{values:make(map[int]int)}
    chang := func(key int,val int)int{
        data.lock.RLock()
        old,ok := data.values[key]  //a1
        data.lock.RUnlock()
        if ok {
            return old
        }

        data.lock.Lock()
        defer data.lock.Unlock()
        data.values[key] = val //a2
        return val
    }
    go chang(0,1)
    go chang(0,2)
}

G1和G2多核下同时执行:

  1. 读锁互不冲突,g1.a1和g2.a1互不冲突,都检测到未设置,要进行a2的操作
  2. 这里问题来了,这里写锁互相冲突,假设g1.a2先执行,已经完成赋值 value[0]=1,这时g2抢到锁后设置values[0]=2是不是不符合代码逻辑?因为values[0]已经赋值过了呀

为了保证上面的先行发生关系,有两种方法:
1.提升锁为互斥锁sync.mutex
2.使用双重检查,即加写锁后再读一次

    chang2 := func(key int,val int)int{
        data.lock.RLock()
        old,ok := data.values[key]
        data.lock.RUnlock()
        if ok {
            return old
        }

        data.lock.Lock()
        defer data.lock.Unlock()
        old,ok = data.values[key]
        if ok {
            return old
        }
        data.values[key] = val
        return val
    }

使用原则小结

  1. 适当减少锁的粒度和范围大小
  2. 尽早释放锁
  3. 避免复制锁
  4. 多线程环境下,注意双重检查
  5. 写多的操作使用Mutex

相关文章

  • go 基础学习

    1 go 基础go 语法基础go 官方资料如果由C ,C++ 基础, 学习go 比较容易,本文学习go ,主要是为...

  • go语言学习

    基础 go的学习,感谢Go By Example、go网络编程与go语言标准库随着学习的深入,此文章持续更新......

  • go语言学习-从基础到实战到源码分析

    收集的一些go语言学习资料,有go基础学习系列,go项目实战,go进阶-go源码分析,还有go的一些书籍,go的架...

  • Go语言高并发Map解决方案

    Go语言高并发Map解决方案 Go语言基础库中的map不是并发安全的,不过基于读写锁可以实现线程安全;不过在Go1...

  • go http学习笔记

    go http学习笔记 @[goweb, go, http] 1.go http基础 go http服务器简例 h...

  • 从0开始Go语言,用Golang搭建网站

    实践是最好的学习方式 零基础通过开发Web服务学习Go语言 本文适合有一定编程基础,但是没有Go语言基础的同学。 ...

  • 初识Go语言-1

    Go语言学习路径 初识Go语言 Go语言环境搭建与IDE安装 Go语言基础语法 Go语言数据类型 Go语言变量和常...

  • go学习第一天

    学习的网站:http://www.runoob.com/go/go-tutorial.html 基础的内容都已经包...

  • Go语言探索 - 3(原创)

    Go语言基础系列博客用到的所有示例代码 在上一篇文章中,我们主要学习了Go语言的编程基础。这些基础内容包括注释、分...

  • Go语言学习 Day 01

    Go语言学习 [TOC] Day 01 基础学习 basic brew install gogo versiong...

网友评论

      本文标题:GO基础学习(13)锁

      本文链接:https://www.haomeiwen.com/subject/iewhsdtx.html