Go Mutex:保护并发访问共享资源的利器

作者:陈明勇
专注分享后端知识,如果本文对你有帮助,不妨点个赞,如果你是 Go 语言初学者,不妨点个关注,一起成长一起进步,如果本文有错误的地方,欢迎指出!
前言
Go 语言以 高并发 著称,其并发操作是重要特性之一。虽然并发可以提高程序性能和效率,但同时也可能带来 竞态条件 和 死锁 等问题。为了避免这些问题,Go 提供了许多 并发原语,例如 Mutex、RWMutex、WaitGroup、Channel 等,用于实现同步、协调和通信等操作。
本文将着重介绍 Go 的 Mutex 并发原语,它是一种锁类型,用于实现共享资源互斥访问。
说明:本文使用的代码基于的 Go 版本:1.20.1
Mutex
基本概念
Mutex 是 Go 语言中互斥锁的实现,它是一种同步机制,用于控制多个 goroutine 之间的并发访问。当多个 goroutine 尝试同时访问同一个共享资源时,可能会导致数据竞争和其他并发问题,因此需要使用互斥锁来协调它们之间的访问。
在上述图片中,我们可以将绿色部分看作是临界区。当 g1 协程通过 mutex 对临界区进行加锁后,临界区将会被锁定。此时如果 g2 想要访问临界区,就会失败并进入阻塞状态,直到锁被释放,g2 才能拿到临界区的访问权。
结构体介绍
字段:
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()方法注释如下所示:
代码示例
我们先来看一个有并发安全问题的例子
在这个例子中,预期的 cnt 结果为 10 * 10000 = 100000。但是由于多个 goroutine 并发访问了共享变量 cnt,并且没有进行任何同步操作,可能导致读写冲突(race condition),从而影响 cnt 的值和输出结果的正确性。这种情况下,不能确定最终输出的 cnt 值是多少,每次执行程序得到的结果可能不同。
在这种情况下,可以使用互斥锁(sync.Mutex)来保护共享变量的访问,保证只有一个 goroutine 能够同时访问 cnt,从而避免竞态条件的问题。修改后的代码如下:
在这个修改后的版本中,使用互斥锁来保护共享变量 cnt 的访问,可以避免出现竞态条件的问题。具体而言,在 cnt++ 操作前,先执行 Lock() 方法,以确保当前 goroutine 获取到了互斥锁并且独占了共享变量的访问权。在 cnt++ 操作完成后,再执行 Unlock() 方法来释放互斥锁,从而允许其他 goroutine 获取互斥锁并访问共享变量。这样,只有一个 goroutine 能够同时访问 cnt,从而确保了最终输出结果的正确性。
易错场景
忘记解锁
如果使用 Lock() 方法之后,没有调用 Unlock() 解锁,会导致其他 goroutine 被永久阻塞。例如:
在上述代码中,通常情况下,cnt 的结果应该为 3。然而没有解锁操作,其中一个 goroutine 被阻塞,导致没有达到预期效果,最终输出的 cnt 可能只能为 1 或 2。
正确的做法是使用 defer 语句在函数返回前释放锁。
重复加锁
重复加锁操作被称为可重入操作。不同于其他一些编程语言的锁实现(例如 Java 的 ReentrantLock),Go 的 mutex 并不支持可重入操作,如果发生了重复加锁操作,就会导致死锁。例如:
在这个例子中,如果在 increase 函数中重复加锁,将会导致 mu 锁被第二次锁住,而其他 goroutine 将被永久阻塞,从而导致程序死锁。正确的做法是只对需要加锁的代码段进行加锁,避免重复加锁。
基于 Mutex 实现一个简单的线程安全的缓存
上述代码实现了一个简单的线程安全的缓存。使用 Mutex 可以保证同一时刻只有一个 goroutine 进行读写操作,避免多个 goroutine 并发读写同一数据时产生数据不一致性的问题。
对于缓存场景,读操作比写操作更频繁,因此使用 RWMutex 代替 Mutex 会更好,因为 RWMutex 允许多个 goroutine 同时进行读操作,只有在写操作时才会进行互斥锁定,从而减少了锁的竞争,提高了程序的并发性能。后续文章会对 RWMutex 进行介绍。
小结
本文主要介绍了 Go 语言中互斥锁 Mutex 的概念、对应的字段和方法、基本使用和易错场景,最后基于 Mutex 实现一个简单的线程安全的缓存。
Mutex 是保证共享资源数据一致性的重要手段,但使用不当会导致性能下降或死锁等问题。因此,在使用 Mutex 时需要仔细考虑代码的设计和并发场景,发挥 Mutex 的最大作用。
版权声明: 本文为 InfoQ 作者【陈明勇】的原创文章。
原文链接:【http://xie.infoq.cn/article/f58b0656102915d44b917a2ec】。文章转载请联系作者。










评论