Golang中的Mutex怎么使用
本篇内容介绍了“Golang中的Mutex怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
Mutex
基本概念
Mutex
是 Go
语言中互斥锁的实现,它是一种同步机制,用于控制多个 goroutine
之间的并发访问。当多个 goroutine
尝试同时访问同一个共享资源时,可能会导致数据竞争和其他并发问题,因此需要使用互斥锁来协调它们之间的访问。
在上述图片中,我们可以将绿色部分看作是临界区。当 g1
协程通过 mutex
对临界区进行加锁后,临界区将会被锁定。此时如果 g2
想要访问临界区,就会失败并进入阻塞状态,直到锁被释放,g2
才能拿到临界区的访问权。
结构体介绍
type Mutex struct { state int32 sema uint32}
字段:
state
state
是一个 int32
类型的变量,它存储着 Mutex
的各种状态信息(未加锁、被加锁、唤醒状态、饥饿状态),不同状态通过位运算进行计算。
sema
sema
是一个信号量,用于实现 Mutex
的等待和唤醒机制。
方法:
Lock()
Lock()
方法用于获取 Mutex
的锁,如果 Mutex
已经被其他的 goroutine
锁定,则 Lock()
方法会一直阻塞,直到该 goroutine
获取到锁为止。
UnLock()
Unlock()
方法用于释放 Mutex
的锁,将 Mutex
的状态设置为未锁定的状态。
TryLock()
Go 1.18
版本以后,sync.Mutex
新增一个 TryLock()
方法,该方法为非阻塞式的加锁操作,如果加锁成功,返回 true
,否则返回 false
。
虽然 TryLock()
的用法确实存在,但由于其使用场景相对较少,因此在使用时应该格外谨慎。TryLock()
方法注释如下所示:
// Note that while correct uses of TryLock do exist, they are rare,// and use of TryLock is often a sign of a deeper problem// in a particular use of mutexes.
代码示例
我们先来看一个有并发安全问题的例子
package mainimport ( "fmt" "sync")var cnt intfunc main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 10000; j++ { cnt++ } }() } wg.Wait() fmt.Println(cnt)}
在这个例子中,预期的 cnt
结果为 10 * 10000 = 100000
。但是由于多个 goroutine
并发访问了共享变量 cnt
,并且没有进行任何同步操作,可能导致读写冲突(race condition
),从而影响 cnt
的值和输出结果的正确性。这种情况下,不能确定最终输出的 cnt
值是多少,每次执行程序得到的结果可能不同。
在这种情况下,可以使用互斥锁(sync.Mutex
)来保护共享变量的访问,保证只有一个 goroutine
能够同时访问 cnt
,从而避免竞态条件的问题。修改后的代码如下:
package mainimport ( "fmt" "sync")var cnt intvar mu sync.Mutexfunc main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 10000; j++ { mu.Lock() cnt++ mu.Unlock() } }() } wg.Wait() fmt.Println(cnt)}
在这个修改后的版本中,使用互斥锁来保护共享变量 cnt
的访问,可以避免出现竞态条件的问题。具体而言,在 cnt++
操作前,先执行 Lock()
方法,以确保当前 goroutine
获取到了互斥锁并且独占了共享变量的访问权。在 cnt++
操作完成后,再执行 Unlock()
方法来释放互斥锁,从而允许其他 goroutine
获取互斥锁并访问共享变量。这样,只有一个 goroutine
能够同时访问 cnt
,从而确保了最终输出结果的正确性。
易错场景
忘记解锁
如果使用 Lock()
方法之后,没有调用 Unlock()
解锁,会导致其他 goroutine
被永久阻塞。例如:
package mainimport ( "fmt" "sync" "time")var mu sync.Mutexvar cnt intfunc main() { go increase(1) go increase(2) time.Sleep(time.Second) fmt.Println(cnt)}func increase(delta int) { mu.Lock() cnt += delta}
在上述代码中,通常情况下,cnt
的结果应该为 3
。然而没有解锁操作,其中一个 goroutine
被阻塞,导致没有达到预期效果,最终输出的 cnt
可能只能为 1
或 2
。
正确的做法是使用 defer
语句在函数返回前释放锁。
func increase(delta int) { mu.Lock() defer mu.Unlock() // 通过 defer 语句在函数返回前释放锁 cnt += delta}
重复加锁
重复加锁操作被称为可重入操作。不同于其他一些编程语言的锁实现(例如 Java
的 ReentrantLock
),Go
的 mutex
并不支持可重入操作,如果发生了重复加锁操作,就会导致死锁。例如:
package mainimport ( "fmt" "sync" "time")var mu sync.Mutexvar cnt intfunc main() { go increase(1) go increase(2) time.Sleep(time.Second) fmt.Println(cnt)}func increase(delta int) { mu.Lock() mu.Lock() cnt += delta mu.Unlock()}
在这个例子中,如果在 increase
函数中重复加锁,将会导致 mu
锁被第二次锁住,而其他 goroutine
将被永久阻塞,从而导致程序死锁。正确的做法是只对需要加锁的代码段进行加锁,避免重复加锁。
基于 Mutex 实现一个简单的线程安全的缓存
import "sync"type Cache struct { data map[string]any mu sync.Mutex}func (c *Cache) Get(key string) (any, bool) { c.mu.Lock() defer c.mu.Unlock() value, ok := c.data[key] return value, ok}func (c *Cache) Set(key string, value any) { c.mu.Lock() defer c.mu.Unlock() c.data[key] = value}
上述代码实现了一个简单的线程安全的缓存。使用 Mutex
可以保证同一时刻只有一个 goroutine
进行读写操作,避免多个 goroutine
并发读写同一数据时产生数据不一致性的问题。
对于缓存场景,读操作比写操作更频繁,因此使用 RWMutex
代替 Mutex
会更好,因为 RWMutex
允许多个 goroutine
同时进行读操作,只有在写操作时才会进行互斥锁定,从而减少了锁的竞争,提高了程序的并发性能。
“Golang中的Mutex怎么使用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341