本文共 3888 字,大约阅读时间需要 12 分钟。
在Go语言的并发编程中,虽然Channel机制是协程间通信的主要工具,但在某些场景下,使用锁是更直观、易于理解的选择。Go语言提供了两种主要的锁类型:互斥锁(Mutex)和读写锁(RWMutex)。本文将详细介绍这两种锁的使用方法,并分析常见的死锁场景。
Mutex的意思是“互斥锁”,即同一时间只能有一个协程执行保护其代码的区域。使用Mutex只需要掌握Lock和Unlock两个方法即可。Lock会阻塞当前协程,直到锁被释放;Unlock则由持有锁的协程释放锁。
在没有加锁的情况下,十个协程同时对一个共享变量进行操作,可能会导致以下问题:
package mainimport ( "fmt" "sync")func main() { var count = 0 var wg sync.WaitGroup n := 10 wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() for j := 0; j < 10000; j++ { count++ } }() } wg.Wait() fmt.Println(count)} 问题分析:由于协程间没有互斥机制,十个协程可能同时修改count变量,导致结果远小于预期(如上述示例中结果为38532,而非100000)。
在加锁后,可以避免多个协程同时修改共享变量,从而确保数据的线性化:
package mainimport ( "fmt" "sync")func main() { var count = 0 var wg sync.WaitGroup var mu sync.Mutex n := 10 wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() for j := 0; j < 10000; j++ { mu.Lock() count++ mu.Unlock() } }() } wg.Wait() fmt.Println(count)} 问题分析:通过Mutex锁,每个协程在加锁和解锁之间保护了count变量的修改,确保了线性化,结果为100000。
Mutex在大量并发场景下可能会导致高阻塞率和性能问题。RWMutex则通过支持多个读者同时持有锁,提升了读操作的并发度,特别适合读密度高、写操作少的场景。
RWMutex遵循以下规则:
RWMutex提供了以下方法:
Lock():加写锁,阻塞直到解锁。Unlock():释放锁。RLock():加读锁,非阻塞。RUnlock():释放读锁。以下示例显示了多个读协程如何在不影响彼此的情况下同时读取数据:
package mainimport ( "fmt" "sync" "time")func main() { var m sync.RWMutex go read(&m, 1) go read(&m, 2) go read(&m, 3) time.Sleep(2 * time.Second)}func read(m *sync.RWMutex, i int) { fmt.Println(i, "reader start") m.RLock() fmt.Println(i, "reading") time.Sleep(1 * time.Second) m.RUnlock() fmt.Println(i, "reader over")} 运行结果:可以看到,三个读协程可以并行执行,而不会互相阻塞。
以下示例展示了读写混合场景下的性能:
package mainimport ( "fmt" "sync" "time")var count = 0func main() { var m sync.RWMutex for i := 1; i <= 3; i++ { go write(&m, i) } for i := 1; i <= 3; i++ { go read(&m, i) } time.Sleep(1 * time.Second) fmt.Println("final count:", count)}func read(m *sync.RWMutex, i int) { fmt.Println(i, "reader start") m.RLock() fmt.Println(i, "reading count:", count) time.Sleep(1 * time.Millisecond) m.RUnlock() fmt.Println(i, "reader over")}func write(m *sync.RWMutex, i int) { fmt.Println(i, "writer start") m.Lock() count++ fmt.Println(i, "writing count", count) time.Sleep(1 * time.Millisecond) m.Unlock() fmt.Println(i, "writer over")} 运行结果:这种配置下,读写混合的并发效率较高,适合大部分场景的需求。
如果Lock和Unlock不成对出现,可能会导致资源无法释放,从而引发死锁。例如:
mu.Lock()defer mu.Unlock()// 可能在此处发生意外,导致Unlock无法执行
问题分析:如果在加锁后发生panic或其他异常,Unlock方法不会被执行,导致锁被死持有。
在某些场景下,锁可能被多次拷贝使用,导致锁无法被正确释放:
package mainimport ( "fmt" "sync")func main() { var mu sync.Mutex mu.Lock() defer mu.Unlock() copyTest(mu)}func copyTest(mu sync.Mutex) { mu.Lock() defer mu.Unlock() fmt.Println("ok")} 问题分析:外层函数已经加锁,在拷贝后又加锁,导致外层的Unlock无法释放锁,这是一个死锁。
类似哲学家就餐问题的场景,A等待B,B等待C,C等待A:
package mainimport ( "sync")func main() { var muA, muB sync.Mutex var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() muA.Lock() defer muA.Unlock() // 依赖B muB.Lock() defer muB.Lock() }() go func() { defer wg.Done() muB.Lock() defer muB.Lock() // 依赖A muA.Lock() defer muA.Unlock() }() wg.Wait()} 问题分析:协程A和B相互等待对方的锁,导致死锁。
通过以上内容,可以看出,Go语言的锁机制虽然提供了强大的工具,但在使用时仍需注意潜在问题,以避免死锁等并发安全问题。
转载地址:http://qzlkz.baihongyu.com/