zoukankan      html  css  js  c++  java
  • Golang 入门系列(十六)锁的使用场景主要涉及到哪些?读写锁为什么会比普通锁快

    前面已经讲过很多Golang系列知识,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.html

    接下来要说的是golang的锁的使用场景主要涉及到哪些?读写锁为什么会比普通锁快。

    一、什么场景下需要用到锁

    当程序中就一个线程的时候,是不需要加锁的,但是通常实际的代码不会只是单线程,有可能是多个线程同时访问公共资源,所以这个时候就需要用到锁了,那么关于锁的使用场景主要涉及到哪些呢?

    1. 多个线程在读相同的数据时
    2. 多个线程在写相同的数据时
    3. 同一个资源,有读又有写时

    二、Go中锁分为两种:

    • 互斥锁 (sync.Mutex)
    • 读写锁 (sync.RWMutex 底层依赖Mutex实现  )

    互斥锁是并发程序对公共资源访问限制最常见的方式。在Go中,sync.Mutex 提供了互斥锁的实现。

    当一个goroutine获得了Mutex后,其他goroutine只能等待,除非该goroutine释放这个Mutex。

    互斥锁结构:

    type Mutex struct {
        state int32
        sema  uint32
    }

    1. 锁定状态值为1,未锁定状态锁未0 。

    2. Lock()加锁、Unlock解锁。

    读写锁则是对读写操作进行加锁。需要注意的是多个读操作之间不存在互斥关系,这样提高了对共享资源的访问效率。

    Go中读写锁由 sync.RWMutex 提供,RWMutex在读锁占用的情况下,会阻止写,但不阻止读。RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占。

    读写锁结构:

    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 int32  // number of pending readers
        readerWait  int32  // number of departing readers
    }

    1. RWMutex是单写多读锁,该锁可以加多个读锁或者一个写锁。

    2. 读锁占用的情况会阻止写,不会阻止读,多个goroutine可以同时获取读锁。

    3. 写锁会阻止其他gorotine不论读或者写进来,整个锁由写锁goroutine占用 与第一条共用示范代码

    4. 适用于读多写少的场景


    三、如何使用互斥锁

    Mutex为互斥锁,Lock() 加锁,Unlock() 解锁,使用Lock() 加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁叶叫做全局锁。

    互斥锁只能锁定一次,当在解锁之前再次进行加锁,便会无法加锁。如果在加锁前解锁,便会报错"panic: sync: unlock of unlocked mutex"。 

    package main
    import ("fmt"
        "sync"
    )
    
    var (
        count int
        lock sync.Mutex
    )
    
    func main() {
        for i := 0; i < 2; i++ {
            go func() {
                for i := 1000000; i > 0; i-- {
                    lock.Lock()
                    count ++
                    lock.Unlock()
                }
                fmt.Println(count)
            }()
        }
    
        fmt.Scanf("
    ") //等待子线程全部结束
    }
    
    运行结果:
    1952533
    2000000 //最后的线程打印输出

    对于上面的程序,a作为一个公共的资源,所以对a的改变、读写等操作都需要加锁。

    需要注意的问题:

      1. 不要重复锁定互斥锁
      2. 不要忘记解锁互斥锁,必要时使用 defer 语句
      3. 不要在多个函数之间直接传递互斥锁

    四、如何使用读写锁

    读写锁的场景主要是在多线程的安全操作下,并且读的情况多于写的情况,也就是说既满足多线程操作的安全性,也要确保性能不能太差,这时候,我们可以考虑使用读写锁。当然你也可以简单暴力直接使用互斥锁(Mutex)。

    Lock() 写锁,如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定。

    Unlock() 写锁解锁,如果没有进行写锁定,则就会引起一个运行时错误。

    RLock() 读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于"读多写少"的场景。

    RUnlock() 读锁解锁,RUnlock 撤销单次RLock 调用,它对于其它同时存在的读取器则没有效果。若 rw 并没有为读取而锁定,调用 RUnlock 就会引发一个运行时错误。

    package main
    import ("fmt"
        "sync"
    )
    
    var (
        count int
        rwLock sync.RWMutex
    )
    
    func main() {
        for i := 0; i < 2; i++ {
            go func() {
                for i := 1000000; i > 0; i-- {
                    rwLock.Lock()
                    count ++
                    rwLock.Unlock()
                }
                fmt.Println(count)
            }()
        }
    
        fmt.Scanf("
    ") //等待子线程全部结束
    }
    
    运行结果:
    1968637
    2000000 

    看着挺复杂的,其实简单来说就是:

    1. 读锁不能阻塞读锁

    2. 读锁需要阻塞写锁,直到所有读锁都释放

    3. 写锁需要阻塞读锁,直到所有写锁都释放

    4. 写锁需要阻塞写锁

    五、最后

    以上,就把golang中各种锁的使用场景及怎么使用互斥锁和读写锁等相关内容介绍完了,希望能对大家有所帮助。

  • 相关阅读:
    排序算法的实现
    图——广度优先遍历(邻接矩阵存储)
    最大子列和问题-4种解法
    PATB 1015. 德才论 (25)
    PATB 1018. 锤子剪刀布
    PATB 1019. 数字黑洞 (20)
    常用协议的默认端口号
    统一资源定位符URL
    hdoj1009 FatMouse' Trade——贪心算法
    hdoj2037 贪心算法——今年暑假不AC
  • 原文地址:https://www.cnblogs.com/zhangweizhong/p/11929509.html
Copyright © 2011-2022 走看看