在 Golang 的并发编程中,锁是一种常用的同步机制,用于确保同一时间只有一个 goroutine 访问共享资源。Golang 中提供了两种锁:sync.Mutex 和 sync.RWMutex。
sync.Mutex 是一种互斥锁,只允许一个 goroutine 获得锁。当一个 goroutine 获得锁后,其他 goroutine 必须等待该 goroutine 释放锁后才能继续执行。sync.RWMutex 是一种读写锁,它允许多个 goroutine 同时读取共享资源,但是只允许一个 goroutine 写入共享资源。当一个 goroutine 拥有写入锁时,其他 goroutine 无法获得读取或写入锁,必须等待该 goroutine 释放锁后才能继续执行。
下面我们详细讲解 Golang 中的锁的使用。
使用 sync.Mutex
sync.Mutex 是一种互斥锁,可以使用 Lock 和 Unlock 方法进行加锁和解锁。
创建一个互斥锁对象
var mutex sync.Mutex
加锁操作
mutex.Lock()
defer mutex.Unlock()
解锁操作
mutex.Unlock()
在加锁的代码块中进行共享资源的操作,操作完成后释放锁。
注意点:
不要在已经加锁的代码块中再次加锁,否则会导致死锁。
加锁和解锁操作需要成对出现,否则会导致锁泄漏或者解锁已经解锁的锁。
互斥锁的使用会带来一定的性能损失,因为锁的获取和释放需要进行系统调用。
互斥锁的使用场景:
互斥锁适用于对共享资源的读写操作进行串行化的场景,例如:
对共享变量进行加减操作
var count intvar mutex sync.Mutex
func increment() {
mutex.Lock()
count++
mutex.Unlock()
}
对共享资源进行读写的场景
var data map[string]intvar mutex sync.Mutex
func readData(key string)int {
mutex.Lock()
defer mutex.Unlock()
return data[key]
}
func writeData(key string, value int) {
mutex.Lock()
defer mutex.Unlock()
data[key] = value
}
在使用互斥锁时需要注意以下几点:
不要将互斥锁作为结构体的成员变量,因为这样会导致锁的拷贝问题。
不要在锁的代码块中进行耗时的操作,否则会影响程序的并发性能。
对于一些读多写少的场景,可以考虑使用读写锁(RWMutex)来替代互斥锁,以提高程序的并发性能。
在使用锁的代码中,尽量避免使用 panic,因为 panic 会破坏锁的语义,导致锁不能正确释放
下面是一个使用互斥锁的案例,
package main
import (
"fmt""sync""time"
)
var count intvar lock sync.Mutex
fun cincrement() {
lock.Lock()
defer lock.Unlock()
count++
}
func main() {
for i := 0; i < 10; i++ {
gofunc() {
for j := 0; j < 100; j++ {
increment()
}
}()
}
time.Sleep(time.Second)
fmt.Println("count:", count)
}
在上面的代码中,我们定义了一个全局变量 count 和一个 Mutex 锁 lock。increment 函数用于增加 count 的值,在该函数中首先调用 lock.Lock() 方法进行加锁,然后使用 defer 关键字在函数返回时调用 lock.Unlock() 方法进行解锁。这样可以确保在任何情况下,加锁和解锁都是成对出现的。
在 main 函数中,我们启动了 10 个 goroutine,每个 goroutine 都会执行 100 次 increment 函数。最后输出 count 的值,由于我们使用了锁来保护 count 的修改,因此最终的输出结果一定是 1000。
使用 sync.RWMutex
在并发编程中,读写锁是一种用于读写分离的机制。在读取数据的时候,可以允许多个线程同时读取数据,但是在写入数据的时候,必须互斥,只能有一个线程进行写操作。
Golang标准库中提供了读写锁的实现,包含两个结构体:
sync.RWMutex:读写锁结构体,包含读锁和写锁;
sync.RLocker:只读锁接口,该接口包含一个只读锁方法RLock()。
在使用读写锁的时候,需要注意以下几点:
对读写锁进行锁定的方法必须是相同的锁类型,不能同时使用读锁和写锁进行锁定,否则会导致死锁。
在读写锁中,读锁是共享的,多个读锁可以同时获得,而写锁是互斥的,只能有一个写锁被获得。因此,在读写锁中,读操作和写操作是不同的。读写锁适用于读多写少的情况。
读写锁不能保证写锁等待的公平性,当一个写锁在等待时,新的读锁可以获得,这可能会导致写锁等待的时间较长。如果需要保证写锁等待的公平性,可以考虑使用互斥锁。
使用读写锁的场景:
读多写少的场景,可以使用读写锁来实现
sync.RWMutex 是一种读写锁,可以使用 RLock 和 RUnlock 方法进行读取锁的加锁和解锁,使用 Lock 和 Unlock 方法进行写入锁的加锁和解锁。
package main
import (
"fmt""sync""time"
)
var count intvar rwlock sync.RWMutex
func increment() {
rwlock.Lock()
defer rwlock.Unlock()
count++
}
func read() {
rwlock.RLock()
defer rwlock.RUnlock()
fmt.Println("count:", count)
}
func main() {
for i := 0; i < 10; i++ {
gofunc() {
for j := 0; j < 10; j++ {
increment()
read()
}
}()
}
time.Sleep(time.Second)
}