Go语言中互斥锁与读写锁,你知多少?

简述Golang中的锁机制主要包含互斥锁和读写锁
互斥锁互斥锁是传统并发程序对共享资源进行控制访问的主要手段 。在Go中主要使用 sync.Mutex的结构体表示 。
一个简单的示例:
func mutex() { var mu sync.Mutex mu.Lock() fmt.Println("locked") mu.Unlock()}或者也可以使用defer来实现,这在整个函数流程中全部要加锁时特别有用,还有一个好处就是可以防止忘记Unlock
func mutex() { var mu sync.Mutex mu.Lock() defer mu.Unlock() fmt.Println("locked")}互斥锁是开箱即用的,只需要申明sync.Mutex即可直接使用
var mu sync.Mutex互斥锁应该是成对出现,在同步语句不可以再对锁加锁,看下面的示例:
func mutex() { var mu sync.Mutex mu.Lock() fmt.Println("parent locked") mu.Lock() fmt.Println("sub locked") mu.Unlock() mu.Unlock()}此时则会出现fatal error: all goroutines are asleep - deadlock!错误
同样,如果多次对一个锁解锁,则会出现fatal error: sync: unlock of unlocked mutex错误
func mutex() { var mu sync.Mutex mu.Lock() fmt.Println("locked") mu.Unlock() mu.Unlock()}那么在goroutine中是否对外部锁加锁呢?
func mutex() { var mu sync.Mutex fmt.Println("parent lock start") mu.Lock() fmt.Println("parent locked") for i := 0; i <= 2; i++ { go func(i int) { fmt.Printf("sub(%d) lock startn", i) mu.Lock() fmt.Printf("sub(%d) lockedn", i) time.Sleep(time.Microsecond * 30) mu.Unlock() fmt.Printf("sub(%d) unlockn", i) }(i) } time.Sleep(time.Second * 2) mu.Unlock() fmt.Println("parent unlock") time.Sleep(time.Second * 2)}先看上面的函数执行结果
parent lock startparent lockedsub(0) lock startsub(2) lock startsub(1) lock startparent unlock // 必须等到父级先解锁,后面则会阻塞sub(0) locked // 解锁后子goroutine才能执行锁定sub(0) unlocksub(2) lockedsub(2) unlocksub(1) lockedsub(1) unlock为了方便调试,使用了time.Sleep()来延迟保证goroutine的执行 从结果中可以看出,当所有的goroutine遇到Lock时都会阻塞,而当main函数中的Unlock执行后,会有一个优先(无序)的goroutine来占得锁,其它的则再次进入阻塞状态 。
总结:

  • 互斥锁必须成对出现
  • 同级别互斥锁不能嵌套使用
  • 父级中如果存在锁,当在goroutine中执行重复锁定操作时goroutine将被阻塞,直到原互斥锁解锁,多个goroutine将会争抢当前锁资源,其它继续阻塞 。
 
Go语言中互斥锁与读写锁,你知多少?

文章插图
 
 
读写锁读写锁和互斥锁不同之处在于,可以分别针对读操作和写操作进行分别锁定,这样对于性能有一定的提升 。读写锁,对于多个写操作,以及写操作和读操作之前都是互斥的这一点基本等同于互斥锁 。但是对于同时多个读操作之前却非互斥关系,这也是相读写锁性能高于互斥锁的主要原因 。
读写锁也是开箱即用型的
var rwm = sync.RWMutex读写锁分为写锁和读锁:
  • 写锁定和写解锁
rwm.Lock()rwm.Unlock()
  • 读锁定和读解锁
rwm.RLock()rwm.RUnlock()读写锁的读锁和写锁不能交叉相互解锁,否则会发生panic,如:
func rwMutex() { var rwm sync.RWMutex rwm.Lock() fmt.Println("locked") rwm.RUnlock()}fatal error: sync: RUnlock of unlocked RWMutex
对于读写锁,同一资源可以同时有多个读锁定,如:
func rwMutex() { var rwm sync.RWMutex rwm.RLock() rwm.RLock() rwm.RLock() fmt.Println("locked") rwm.RUnlock() rwm.RUnlock() rwm.RUnlock()}但对于写锁定只能有一个(和互斥锁相同),同时使用多个会产生deadlock的panic,如:
func rwMutex() { var rwm sync.RWMutex rwm.Lock() rwm.Lock() rwm.Lock() fmt.Println("locked") rwm.Unlock() rwm.Unlock() rwm.Unlock()}在goroutine中,写解锁会试图唤醒所有想要进行读锁定而被阻塞的goroutine 。
而读解锁会在已无任何读锁定的情况下,试图唤醒一个想进行写锁定而被阻塞的goroutine 。
下面看一个完整示例:
func rwMutex() { var rwm sync.RWMutex for i := 0; i <= 2; i++ { go func(i int) { fmt.Printf("go(%d) start lockn", i) rwm.RLock() fmt.Printf("go(%d) lockedn", i) time.Sleep(time.Second * 2) rwm.RUnlock() fmt.Printf("go(%d) unlockn", i) }(i) } // 先sleep一小会,保证for的goroutine都会执行 time.Sleep(time.Microsecond * 100) fmt.Println("main start lock") // 当子进程都执行时,且子进程所有的资源都已经Unlock了 // 父进程才会执行 rwm.Lock() fmt.Println("main locked") time.Sleep(time.Second) rwm.Unlock()}go(0) start lockgo(0) lockedgo(1) start lockgo(1) lockedgo(2) start lockgo(2) lockedmain start lockgo(2) unlockgo(0) unlockgo(1) unlockmain locked


推荐阅读