Golang 并发安全和锁:互斥锁、读写锁
互斥锁
var wg sync.WaitGroup func main() { for i := 0; i < 20; i++ { wg.Add(1) go test() } wg.Wait() } var count = 0 // 定义count func test() { count++ fmt.Println(count) wg.Done() } // 普通编译没有问题,但如果使用 go build -race main.go 进行编译,运行,会发现存在竞争情况 Found 1 data race(s) // 此时,可以使用互斥锁来解决多个协程同时操作的情况互斥锁,是传统并发编程中,对共享资源进行访问控制的主要手段
- Golang 中,互斥锁,使用 sync.Mutex 结构体表示,该结构体只有两个公开的指针方法:
- Lock(),锁定当前的共享资源
- Unlock(),进行解锁
使用互斥锁,能够保证同一时间有且只有一个协程进入临界区,其他的协程则在等待解锁,解锁后,等待的协程才能获取锁,然后进入临界区;另,多个协程等待一个锁时,唤醒的策略是随机的
var wg sync.WaitGroup var mutex sync.Mutex // mutex,互斥的意思 func main() { for i := 0; i < 20; i++ { wg.Add(1) go test() } wg.Wait() } var count = 0 // 定义count // 使用互斥锁,锁定当前的共享资源,则一次只能一个协程操作 func test() { mutex.Lock() // 加锁 count++ fmt.Println(count) wg.Done() mutex.Unlock() // 解锁 }
读写( 互斥 )锁
互斥锁的本质是,当一个协程访问的时候,其他协程都不能访问。但是,在资源同步、避免竞争的同时,也降低了程序的并发性能,程序由之前的并行执行,变成了串行执行
实际上,真正的互斥,应该在写( 读取和修改、修改和修改 ),而读,是没有互斥操作的必要的
- 当我们对一个不会变化的数据,进行读操作的时候,是不会存在资源竞争的问题的,因为数据是不变的,不管怎么读取,多少个协程同时读取,都是可以的
- 问题主要出行在 “修改”,也就是写操作上,修改的数据要同步,这样其他的协程才会感知到
因此,衍生出了另外一种锁,读写( 互斥 )锁,其特点是:
- 多个读操作,可以并发执行
- 但是,对于写操作是完全互斥的,即,一个协程进行写操作时,其他协程既不能进行读操作,也不能进行写操作
「 基本语法 」读写锁,由结构体 sync.RWMutex 表示,该结构体的方法集合中,包含两对方法:
- 对写操作的加锁和解锁 func (*RWMutex)Lock()、func (*RWMutex)Unlock()
- 对读操作的解锁和解锁 func (*RWMutex)RLock()、func (*RWMutex)RUnlock()
var wg sync.WaitGroup var mutex sync.RWMutex func main() { // 开启10个协程进行读操作 for i := 0; i < 10; i++ { wg.Add(1) go read() } // 开启10个协程进行写操作 for i := 0; i < 10; i++ { wg.Add(1) go write() } wg.Wait() } // 写操作 func write() { mutex.Lock() // 加锁 fmt.Println("写操作") time.Sleep(time.Second) wg.Done() mutex.Unlock() // 解锁 } // 读操作 func read() { mutex.RLock() // 读写锁 - 加锁 fmt.Println("读操作") time.Sleep(time.Second) wg.Done() mutex.RUnlock() // 读写锁 - 解锁 }