1. 简介
本文的主要内容是介绍Go中Mutex并发原语。包含Mutex的基本使用,使用的注意事项以及一些实践建议。
2. 基本使用
2.1 基本定义
Mutex是Go语言中的一种同步原语,全称为Mutual Exclusion,即互斥锁。它可以在并发编程中实现对共享资源的互斥访问,保证同一时刻只有一个协程可以访问共享资源。Mutex通常用于控制对临界区的访问,以避免竞态条件的出现。
2.2 使用方式
使用Mutex的基本方法非常简单,可以通过调用Mutex的Lock方法来获取锁,然后通过Unlock方法释放锁,示例代码如下:
import "sync"
var mutex sync.Mutex
func main() {
mutex.Lock() // 获取锁
// 执行需要同步的操作
mutex.Unlock() // 释放锁
}
2.3 使用例子
2.3.1 未使用mutex同步代码示例
下面是一个使用goroutine访问共享资源,但没有使用Mutex进行同步的代码示例:
package main
import (
"fmt"
"time"
)
var count int
func main() {
for i := 0; i < 1000; i++ {
go add()
}
time.Sleep(1 * time.Second)
fmt.Println("count:", count)
}
func add() {
count++
}
上述代码中,我们启动了1000个goroutine,每个goroutine都调用add()函数将count变量的值加1。由于count变量是共享资源,因此在多个goroutine同时访问的情况下会出现竞态条件。但是由于没有使用Mutex进行同步,所以会导致count的值无法正确累加,最终输出的结果也会出现错误。
在这个例子中,由于多个goroutine同时访问count变量,而不进行同步控制,导致每个goroutine都可能读取到同样的count值,进行相同的累加操作。这就会导致最终输出的count值不是期望的结果。如果我们使用Mutex进行同步控制,就可以避免这种竞态条件的出现。
2.3.2 使用mutex解决上述问题
下面是使用Mutex进行同步控制,解决上述代码中竞态条件问题的示例:
package main
import (
"fmt"
"sync"
"time"
)
var (
count int
mutex sync.Mutex
)
func main() {
for i := 0; i < 1000; i++ {
go add()
}
time.Sleep(1 * time.Second)
fmt.Println("count:", count)
}
func add() {
mutex.Lock()
count++
mutex.Unlock()
}
在上述代码中,我们在全局定义了一个sync.Mutex类型的变量mutex,用于进行同步控制。在add()函数中,我们首先调用mutex.Lock()方法获取mutex的锁,确保只有一个goroutine可以访问count变量。然后进行加1操作,最后调用mutex.Unlock()方法释放mutex的锁,使其他goroutine可以继续访问count变量。
通过使用Mutex进行同步控制,我们避免了竞态条件的出现,确保了count变量的正确累加。最终输出的结果也符合预期。
3. 使用注意事项
3.1 Lock/Unlock需要成对出现
下面是一个没有成对出现Lock和Unlock的代码例子:
package main
import (
"fmt"
"sync"
)
func main() {
var mutex sync.Mutex
go func() {
mutex.Lock()
fmt.Println("goroutine1 locked the mutex")
}()
go func() {
fmt.Println("goroutine2 trying to lock the mutex")
mutex.Lock()
fmt.Println("goroutine2 locked the mutex")
}()
}
在上述代码中,我们创建了一个sync.Mutex类型的变量mutex,然后在两个goroutine中使用了这个mutex。
在第一个goroutine中,我们调用了mutex.Lock()方法获取mutex的锁,但是没有调用相应的Unlock方法。在第二个goroutine中,我们首先打印了一条信息,然后调用了mutex.Lock()方法尝试获取mutex的锁。由于第一个goroutine没有释放mutex的锁,第二个goroutine就一直阻塞在Lock方法中,一直无法执行。
因此,在使用Mutex的过程中,一定要确保每个Lock方法都有对应的Unlock方法,确保Mutex的正常使用。
3.2 不能对已使用的Mutex作为参数进行传递
下面举一个已使用的Mutex作为参数进行传递的代码的例子:
type Counter struct {
sync.Mutex
Count int
}
func main(){
var c Counter
c.Lock()
defer c.Unlock()
c.Count++
foo(c)
fmt.println("done")
}
func foo(c Counter) {
c.Lock()
defer c.Unlock()
fmt.println("foo done")
}
当一个 mutex 被传递给一个函数时,预期的行为应该是该函数在访问受 mutex 保护的共享资源时,能够正确地获取和释放 mutex,以避免竞态条件的发生。
如果我们在Mutex未解锁的情况下拷贝这个Mutex,就会导致锁失效的问题。因为Mutex的状态信息被拷贝了,拷贝出来的Mutex还是处于锁定的状态。而在函数中,当要访问临界区数据时,首先肯定是先调用Mutex.Lock方法加锁,而传入Mutex其实是处于锁定状态的,此时函数将永远无法获取到锁。
因此,不能将已使用的Mutex直接作为参数进行传递。
3.3 不可重复调用Lock/UnLock方法
下面是一个例子,其中对同一个 Mutex 进行了重复加锁:
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
fmt.Println("First Lock")
// 重复加锁
mu.Lock()
fmt.Println("Second Lock")
mu.Unlock()
mu.Unlock()
}
在这个例子中,我们先对 Mutex 进行了一次加锁,然后在没有解锁的情况下,又进行了一次加锁操作.