Golang中的锁
锁是一种在并发编程中常用的同步机制,用于管理对共享资源的访问。Golang提供了多种锁类型,可以满足不同的并发编程需求。
用途和分类
锁的主要作用是保护共享资源,防止多个goroutine同时访问同一个资源,导致数据的不一致性和并发安全问题。Golang提供的锁主要分为两类:
- 互斥锁:sync.Mutex
互斥锁是最基本的锁类型,可以用于保护任何类型的共享资源。当一个goroutine获取到互斥锁时,其他goroutine必须等待它释放锁后才能获取。互斥锁的主要方法是Lock()
和Unlock()
。 - 读写锁:sync.RWMutex
读写锁可以分为读锁和写锁两种类型。多个goroutine可以同时获取读锁,但只有一个goroutine可以获取写锁。读写锁适用于读多写少的场景,可以提高并发性能。读写锁的主要方法是RLock()
、RUnlock()
、Lock()
和Unlock()
。
基础使用
互斥锁
package main
import (
"fmt"
"sync"
)
var count int
var mutex sync.Mutex
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mutex.Lock()
count++
mutex.Unlock()
}()
}
wg.Wait()
fmt.Println(count)
}
上面的代码演示了如何使用互斥锁保护一个共享变量count
,并发地对其进行加一操作。需要注意的是,在使用互斥锁时,需要在临界区获取锁并在临界区结束时释放锁。
读写锁
package main
import (
"fmt"
"sync"
)
var count int
var rwMutex sync.RWMutex
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
rwMutex.RLock()
fmt.Println(count)
rwMutex.RUnlock()
}()
}
wg.Add(1)
go func() {
defer wg.Done()
rwMutex.Lock()
count++
rwMutex.Unlock()
}()
wg.Wait()
}
上面的代码演示了如何使用读写锁保护一个共享变量count
,并发地进行读和写操作。在读操作时使用RLock()
获取读锁,在写操作时使用Lock()
获取写锁。
例子
例子一:并发map
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
m map[string]int
sync.Mutex
}
func (sm *SafeMap) Get(key string) int {
sm.Lock()
defer sm.Unlock()
return sm.m[key]
}
func (sm *SafeMap) Set(key string, value int) {
sm.Lock()
defer sm.Unlock()
sm.m[key] = value
}
func main() {
sm := &SafeMap{m: make(map[string]int)}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key%d", i), i)
}(i)
}
wg.Wait()
fmt.Println(sm.Get("key99"))
}
上面的代码演示了如何使用互斥锁实现一个并发安全的map,保证多个goroutine对map的操作是安全的。
例子二:读写锁优化
package main
import (
"fmt"
"sync"
"time"
)
type SafeCounter struct {
count int
rwMutex sync.RWMutex
}
func (sc *SafeCounter) Inc() {
sc.rwMutex.Lock()
defer sc.rwMutex.Unlock()
sc.count++
}
func (sc *SafeCounter) Value() int {
sc.rwMutex.RLock()
defer sc.rwMutex.RUnlock()
return sc.count
}
func main() {
var wg sync.WaitGroup
sc := &SafeCounter{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
sc.Inc()
}
}()
}
wg.Wait()
fmt.Println(sc.Value())
}
上面的代码演示了如何使用读写锁优化并发累加器的性能。通过使用读写锁,可以使多个goroutine同时读取计数器的值,而只有一个goroutine可以更新计数器的值。
例子三:条件变量
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var cond sync.Cond
var count int
func main() {
cond = sync.Cond{L: &sync.Mutex{}}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cond.L.Lock()
for count < 5 {
cond.Wait()
}
fmt.Println("goroutine is running")
cond.L.Unlock()
}()
}
time.Sleep(1 * time.Second)
cond.L.Lock()
count = 5
cond.Broadcast()
cond.L.Unlock()
wg.Wait()
}
上面的代码演示了如何使用条件变量实现多个goroutine之间的同步。在该例子中,有10个goroutine等待条件变量,只有在变量满足一定条件时才能继续执行。当条件变量满足条件后,使用Broadcast()
通知所有等待的goroutine。
适用场景和建议
适用场景:
- 有多个goroutine需要同时访问共享资源,并发安全是必须的。
- 读多写少的场景,使用读写锁可以提高并发性能。
- 需要实现多个goroutine之间的同步和通信。
建议:
- 在使用锁时,需要注意死锁的问题。当多个goroutine在等待获取锁的时候,可能会导致死锁。
- 避免使用过多的锁,锁的数量越多,越容易出现死锁等问题。
- 在使用互斥锁时,要尽量减少锁的粒度,即尽可能让锁的范围更小,以提高并发性能。
- 在使用读写锁时,需要根据实际情况选择读锁和写锁的数量,避免读锁和写锁的竞争,从而提高并发性能。
- 在使用条件变量时,需要注意使用
Wait()
和Signal()
或Broadcast()
的顺序和条件的更新方式,避免出现死锁和竞争等问题。
在并发编程中,锁是一个非常重要的概念,掌握锁的使用方法和注意事项,对于提高程序的并发性能和安全性具有非常重要的作用。