并发安全和锁
1、竞态问题
多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题。
2、互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用sync
包中提供的Mutex
类型来实现互斥锁。
sync.Mutex
提供了两个方法供我们使用。
方法名 | 功能 |
---|---|
func (m *Mutex) Lock() | 获取互斥锁 |
func (m *Mutex) Unlock() | 释放互斥锁 |
package main import ( "fmt" "sync" ) // sync.Mutex var ( x int64 wg sync.WaitGroup // 等待组 m sync.Mutex // 互斥锁 ) // add 对全局变量x执行5000次加1操作 func add() { for i := 0; i < 5000; i++ { m.Lock() // 修改x前加锁 x = x + 1 m.Unlock() // 改完解锁 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) }
3、读写互斥锁
互斥锁是完全互斥的,但是实际上有很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使用读写锁是更好的一种选择。读写锁在 Go 语言中使用sync
包中的RWMutex
类型。
sync.RWMutex
提供了以下5个方法。
方法名 | 功能 |
---|---|
func (rw *RWMutex) Lock() | 获取写锁 |
func (rw *RWMutex) Unlock() | 释放写锁 |
func (rw *RWMutex) RLock() | 获取读锁 |
func (rw *RWMutex) RUnlock() | 释放读锁 |
func (rw *RWMutex) RLocker() Locker | 返回一个实现Locker接口的读写锁 |
读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
var ( x int64 wg sync.WaitGroup mutex sync.Mutex rwMutex sync.RWMutex ) // writeWithLock 使用互斥锁的写操作 func writeWithLock() { mutex.Lock() // 加互斥锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒 mutex.Unlock() // 解互斥锁 wg.Done() } // readWithLock 使用互斥锁的读操作 func readWithLock() { mutex.Lock() // 加互斥锁 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒 mutex.Unlock() // 释放互斥锁 wg.Done() } // writeWithLock 使用读写互斥锁的写操作 func writeWithRWLock() { rwMutex.Lock() // 加写锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒 rwMutex.Unlock() // 释放写锁 wg.Done() } // readWithRWLock 使用读写互斥锁的读操作 func readWithRWLock() { rwMutex.RLock() // 加读锁 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒 rwMutex.RUnlock() // 释放读锁 wg.Done() } func do(wf, rf func(), wc, rc int) { start := time.Now() // wc个并发写操作 for i := 0; i < wc; i++ { wg.Add(1) go wf() } // rc个并发读操作 for i := 0; i < rc; i++ { wg.Add(1) go rf() } wg.Wait() cost := time.Since(start) fmt.Printf("x:%v cost:%v\n", x, cost) }
使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来。
参考:Go语言基础之并发 | 李文周的博客 (liwenzhou.com)