美文网首页go语言
Go-同步原语与锁(一)互斥锁与读写锁

Go-同步原语与锁(一)互斥锁与读写锁

作者: 链人成长chainerup | 来源:发表于2019-09-25 23:11 被阅读0次

    本文将讲解一下Go语言中的同步原语与锁。会阐述几种常见的锁,剖析其流程,然后针对每种同步原语举几个例子。由于文章比较长,为方便阅读,将这部分拆解为两部分。本文是第一部分 互斥锁与读写锁。

    环境: go version go1.8.7 darwin/amd64

    1 mutex

    1.1 结构

    先看下mutex在底层的结构: 代码位置sync/mutex.go

    type Mutex struct {
        state int32 // 状态
        sema  uint32 // 信号
    }
    

    在mutex中 主要有mutexLocked , mutexWorken 两种状态,mutexWaiterShift 是统计了等待mutex的routine 数量。

    1.2 流程

    参考文献中讲到Go1.9 针对互斥锁增加了饥饿模式,我们这儿不讨论了哈。

    1.2.1 Lock

    流程大体是这样子的:


    mutex-lock.png
    1.2.2 Unlock
    mutex-unlock.png

    1.3 例子

    package main
    
    import (
        "fmt"
        "math/rand"
        "sync"
        "testing"
        "time"
    )
    
    func TestMutex(t *testing.T) {
        lock := sync.Mutex{}
        var a map[int]int
        a = make(map[int]int, 5)
    
        a[8] = 10
        a[3] = 10
        a[2] = 10
        a[1] = 10
        a[18] = 10
    
        for i := 0; i < 2; i++ {
            go func(b map[int]int) {
                lock.Lock()
                b[8] = rand.Intn(100)
                lock.Unlock()
            }(a)
        }
        lock.Lock()
        fmt.Println("main routine print in mutex......")
        fmt.Println(a)
        lock.Unlock()
    
        time.Sleep(time.Second)
        fmt.Println("main routine print at end......")
        fmt.Println(a)
    }
    
    

    结论是:

    main routine print in mutex......
    map[8:10 3:10 2:10 1:10 18:10]
    main routine print at end......
    map[2:10 1:10 18:10 8:87 3:10]
    

    2 RWmutex

    RWmutex 读写锁,对共享资源的”读操作“ 和”写操作“ 进行了区分,更加细化。
    其基本的原则是:

    对某个受到读写锁保护的共享资源,多个写不能同时操作,读写也不可以同时操作,但是多个读可以同时进行。

    2.1 结构

    type RWMutex struct {
        w           Mutex  // 写操作之间的互斥锁
        writerSem   uint32 // semaphore for writers to wait for completing readers
        readerSem   uint32 // semaphore for readers to wait for completing writers
        readerCount int32  // 正在进行的读操作的数量
        readerWait  int32  // 当写操作被阻塞时等待的读操作个数
    }
    

    每个字段的含义在注释里面哈~

    2.2 流程

    这部分我们只做简单的流程分析。

    2.2.1 读锁的获取
    RWmutex-Rlock-get.png
    2.2.2 读锁的释放
    RWmutex-Rlock-release.png
    2.2.3 写锁的获取
    • 此时有读锁:进入休眠,等待被唤醒
    • 此时有写锁:互斥,进入等待状态
    • 此时没有读锁、写锁:获取写锁
    2.2.3 写锁的释放

    先释放写锁,然后看有没有在等待中的读操作:如果有读操作在等待,先让读锁执行;如果没有读操作,就释放写锁,让其他写锁获取当前锁。这样操作是防止读操作被饿死。

    2.3 例子

    先来个”读-读“的例子:

    func TestRWMutex_multiRead(t *testing.T) {
        m = new(sync.RWMutex)
    
        // 多个同时读
        now := time.Now().Nanosecond()
        go read(1, now)
        now2 := time.Now().Nanosecond()
        go read(2,now2)
    
        time.Sleep(2*time.Second)
    }
    func read(i int, beginTime int) {
        println(time.Now().Nanosecond() - beginTime, i,"read start")
    
        m.RLock()
        println(time.Now().Nanosecond() - beginTime, i,"reading")
        time.Sleep(1*time.Second)
        println(time.Now().Nanosecond() - beginTime, i,"read over")
        m.RUnlock()
    }
    

    结论是:

    16000 1 read start
    27000 1 reading
    28000 2 read start
    38000 2 reading
    3159000 2 read over
    3167000 1 read over
    

    我们看到,reader1 没有结束,reader2就开始读了,验证了我们的读-读是可以并存的。

    再来个”读-写混合“的例子:

    
    func TestRWMutex_WriteRead(t *testing.T) {
        m = new(sync.RWMutex)
        beginTime := time.Now().Nanosecond()
        // 有读有写: 读-写-写-读
        go read(1, beginTime)
        go write(2, beginTime)
        go write(3, beginTime)
        go read(4, beginTime)
    
        time.Sleep(5*time.Second)
    }
    
    func read(i int, beginTime int) {
        println(time.Now().Nanosecond() - beginTime, i,"read start")
    
        m.RLock()
        println(time.Now().Nanosecond() - beginTime, i,"reading")
        time.Sleep(1*time.Second)
        println(time.Now().Nanosecond() - beginTime, i,"read over")
        m.RUnlock()
    }
    
    func write(i int,  beginTime int)  {
        println(time.Now().Nanosecond() - beginTime, i,"write start")
    
        m.Lock()
        println(time.Now().Nanosecond() - beginTime, i,"writing")
        time.Sleep(1*time.Second)
        println(time.Now().Nanosecond() - beginTime, i,"write over")
        m.Unlock()
    
    }
    

    看下执行某一次的结果:

    10000 2 write start
    24000 2 writing
    42000 4 read start
    44000 1 read start
    20000 3 write start
    1926000 2 write over
    1955000 4 reading
    1968000 1 reading
    5641000 1 read over
    5648000 4 read over
    5714000 3 writing
    10094000 3 write over
    

    我们看到 先执行了写操作2,等到写操作2执行完毕之后,读操作4跟1才开始执行,等这俩读都执行完,最后才执行了写操作3

    3 小结

    本文是Go-同步原语与锁的第一部分: 互斥锁与读写锁。从结构、获取释放锁、例子三个维度进行了讲解。

    4 参考文献

    同步原语与锁 https://draveness.me/golang/concurrency/golang-sync-primitives.html
    Go 1.8 源码

    5 其他

    本文是《循序渐进go语言》的第十三篇-《Go-同步原语与锁(一)互斥锁与读写锁》。
    如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~

    相关文章

      网友评论

        本文标题:Go-同步原语与锁(一)互斥锁与读写锁

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