1. golang锁简介
golang的并发编程是其最大的特点之一,在并发编程中,为了保证并发安全性,我们通常需要使用锁。
golang提供了两种锁:sync.Mutex 和 sync.RWMutex,分别对应互斥锁和读写锁,其中互斥锁是最基本的锁,在所有锁中它的代价最高,但相对的其使用的也较为简单。
2. golang锁基本用法
2.1 sync.Mutex用法
互斥锁最简单的用法就是调用锁的Lock()方法和Unlock()方法进行上锁和解锁操作。
下面是一个简单的例子:
// 创建一个互斥锁
var mutex = &sync.Mutex{}
// 定义一个共享资源
var num int
// 启动10个协程对num进行++运算
for i:=0; i < 10; i++ {
go func() {
mutex.Lock()
defer mutex.Unlock()
num++
}()
}
// 等待所有协程执行完毕
time.Sleep(1 * time.Second)
fmt.Println(num)
在上面的例子中,通过mutex.Lock()方法上锁,使得对共享资源num的操作变为原子操作。这样保证了数据的正确性。需要注意的是,每次对共享资源进行操作之后,一定要调用mutex.Unlock()方法进行解锁,否则会导致协程死锁。
2.2 sync.RWMutex用法
读写锁与互斥锁不同,它分为两种操作:读操作和写操作,读操作可以同时进行,但与写操作互斥,读写锁主要应用于读多写少的场景,在这种场景下,读写锁能够显著提高程序的并发性。
下面是一个简单的例子:
// 创建读写锁
var rwmutex = &sync.RWMutex{}
// 定义一个共享资源
var arr = make([]int, 0)
// 添加操作,加上写锁
func add(i int) {
rwmutex.Lock()
defer rwmutex.Unlock()
arr = append(arr, i)
}
// 读取操作,加上读锁
func read() {
rwmutex.RLock()
defer rwmutex.RUnlock()
for _, v := range arr {
fmt.Println(v)
}
}
// 启动10个协程分别对arr进行添加操作
for i:=0; i < 10; i++ {
go func() {
add(i)
}()
}
// 等待所有协程执行完毕
time.Sleep(1 * time.Second)
// 读取操作
read()
3. golang锁的复制问题
golang的锁是不能进行复制的。这是因为锁中通常存储的有一些状态信息,比如是否已上锁等信息。如果直接进行复制,锁的状态信息也会跟随被复制,导致状态信息的不一致性,从而导致程序的并发安全性受到威胁。
下面是一个简单的例子:
type MyMutex struct {
sync.Mutex
}
func main() {
mutex1 := &MyMutex{}
// copy mutex1 to mutex2
mutex2 := mutex1
// mutex1 lock
mutex1.Lock()
defer mutex1.Unlock()
// mutex2 lock !!!deadlock!!!
mutex2.Lock() // 死锁
defer mutex2.Unlock()
}
在上面的代码中,我们定义了一个名为MyMutex的结构体,这个结构体内部封装了一个sync.Mutex类型的嵌入对象。该结构体的定义相当于对sync.Mutex做了一层封装。
在main函数中,我们将mutex1的复制给了mutex2,之后分别对mutex1和mutex2进行了上锁操作。由于mutex1和mutex2引用的是同一个对象,所以对mutex1的上锁操作会导致mutex2也被锁住。这样就会导致死锁问题的出现。
4. 如何避免锁的复制问题
在golang中,锁不能进行复制,这就要求我们在使用锁的时候,需要特别注意避免锁的复制问题。下面给出一些常见的避免方法。
4.1 封装结构体
通过封装结构体的方式,将锁作为结构体的成员,避免直接对锁进行复制。
type MyMutex struct {
sync.Mutex
}
type MyData struct {
mu MyMutex
arr []int
}
func main() {
data1 := &MyData{
mu: MyMutex{},
}
data2 := &MyData{
mu: MyMutex{},
}
// data1 lock
data1.mu.Lock()
defer data1.mu.Unlock()
// data2 lock
data2.mu.Lock()
defer data2.mu.Unlock()
}
在上面的例子中,我们通过封装结构体的方式避免了锁的复制问题。锁通过Mutex的嵌入方式被封装为MyMutex结构体的成员,这样就避免直接对锁进行了复制。
4.2 使用指针方式传递
通过使用指针方式,避免对锁进行复制。
type MyData2 struct {
mu *sync.Mutex
arr []int
}
func main() {
mu := &sync.Mutex{}
data1 := &MyData2{
mu: mu,
}
data2 := &MyData2{
mu: mu,
}
// data1 lock
data1.mu.Lock()
defer data1.mu.Unlock()
// data2 lock
data2.mu.Lock()
defer data2.mu.Unlock()
}
在上面的例子中,我们通过指针方式传递锁,避免了锁的复制问题。在MyData2结构体中,我们使用指针mu来存储锁,这样就避免了锁的复制。
4.3 使用sync包中的Once
Once是一个通过Do方法保证只执行一次的类型。在once中,Mutex被嵌入,而其他的状态信息则通过一个布尔值once和一个指针done来表示。
type MyData3 struct {
once sync.Once
arr []int
}
func main() {
data1 := &MyData3{}
data2 := &MyData3{}
// data1 lock
data1.once.Do(func() {
fmt.Println("data1 lock")
})
// data2 lock
data2.once.Do(func() {
fmt.Println("data2 lock")
})
}
在上面的例子中,我们通过使用sync包中的Once来避免了锁的复制问题。在MyData3结构体中,我们使用一个Once对象once作为锁,当Once对象中状态为尚未done时,通过Do方法来进行操作。
5. 总结
golang中锁是保证并发安全的重要手段之一,但是锁不能进行复制,避免锁的复制成为了我们在使用锁时需要特别注意的问题,通过封装结构体、使用指针方式传递等方式可以有效地避免锁的复制问题。