Go 语言快速入门指南 -- Go 并发互斥锁
互斥是并发编程中最关键的概念之一。当我们使用 goruntine 和 channels 进行并发编程时,如果两个 goruntine 尝试同时访问同一个内存位置的同一数据会发生竞争,有时候会产生意想不到的结果,通常很难调试,不符合日常要求,出现错误甚至很难修复。生活场景假设在生活中可能会发生的例子:有一个银行系统,我们可以从银行余额中存款和取款。在一个单线程的同步程序中,这个操作很简单。我们可以通过少量的单元测试有效地保证它每次都能按计划工作。然而,如果我们开始引入多个线程,在 Go 语言中使用多个 goroutine,我们可能会开始在我们的代码中看到问题。
假如有一个余额为 1000 元的客户。
客户将 500 元存入他的账户。
一个 goroutine 会看到这个交易,读取价值为 1000 ,并继续将 500 添加到现有的余额中。(此时应该是 1500 的余额)
然而,在同一时刻,他拿 800 元来还分期付款的 iphone 13.
第二个程序在第一个程序能够增加 500 元的额外存款之前,读取了 1000 元的账户余额,并继续从他的账户中扣除 800 元。(1000 - 800 = 200)
第二天,客户检查了他的银行余额,发现他的账户余额减少到了 200 元,因为第二个程序没有意识到第一笔存款,并在存款完成前做了扣除操作。这就是一个线程竞赛的例子,如果我们不小心落入这样的代码,我们的并发程序就会出现问题。互斥锁和读写锁互斥锁,英文名 Mutex,顾名思义,就是相互排斥,是保护程序中临界区的一种方式。而临界区是程序中需要独占访问共享资源的区域。互斥锁提供了一种安全的方式来表示对这些共享资源的独占访问。为了使用资源,channel 通过通信共享内存,而 Mutex 通过开发人员的约定同步访问共享内存。
让我们看一个没有 Mutex 的并发编程示例 package main
import ("fmt""sync")
type calculation struct {sum int}
func main() {
}
func dosomething(test *calculation, wg *sync.WaitGroup) {test.sum++wg.Done()}第一次结果为:491
第二次结果:493[Running] go run "e:\Coding Workspaces\LearningGoTheEasiestWay\concurrency\mutex\v0\main.go"493 在上面的例子中,我们声明了一个名为 test 的计算结构体,并通过 for 循环产生了多个 GoRoutines,将 sum 的值加 1。(如果你对 GoRoutines 和 WaitGroup 不熟悉,请参考之前的教程)。 我们可能期望 for 循环后 sum 的值应该是 500。然而,这可能不是真的。 有时,您可能会得到小于 500(当然永远不会超过 500)的结果。 这背后的原因是两个 GoRoutine 有一定的概率在相同的内存位置操作相同的变量,从而导致这种意外结果。 这个问题的解决方案是使用互斥锁。
使用 Mutexpackage main
import ("fmt""sync")
type calculation struct {sum intmutex sync.Mutex}
func main() {
}
func dosomething(test *calculation, wg *sync.WaitGroup) {test.mutex.Lock()test.sum++test.mutex.Unlock()wg.Done()}结果为:[Running] go run "e:\Coding Workspaces\LearningGoTheEasiestWay\concurrency\mutex\v0.1\main.go"500 在第二个示例中,我们在结构中添加了一个互斥锁属性,它是一种类型的 sync.Mutex。然后我们使用互斥锁的 Lock() 和 Unlock() 来保护 test.sum 当它被并发修改时,即 test.sum++。请记住,使用互斥锁并非没有后果,因为它会影响应用程序的性能,因此我们需要适当有效地使用它。 如果你的 GoRoutines 只读取共享数据而不写入相同的数据,那么竞争条件就不会成为问题。 在这种情况下,您可以使用 RWMutex 代替 Mutex 来提高性能时间。
Defer 关键字对 Unlock() 使用 defer 关键字通常是一个好习惯。func dosomething(test *calculation) error{test.mutex.Lock()defer test.mutex.Unlock()
}在这种情况下,我们有多个 if err!=nil 这可能会导致函数提前退出。 通过使用 defer,无论函数如何返回,我们都可以保证释放锁。 否则,我们需要将 Unlock() 放在函数可能返回的每个地方。 然而,这并不意味着我们应该一直使用 defer。 让我们再看一个例子。func dosomething(test *calculation){test.mutex.Lock()defer test.mutex.Unlock()
}在这个例子中,mutex 不会释放锁,直到耗时的函数(这里是 http.Get())完成。 在这种情况下,我们可以在 test.sum=... 行之后解锁互斥锁,因为这是我们操作变量的唯一地方。总结很多时候 Mutex 并不是单独使用的,而是嵌套在 Struct 中使用,作为结构体的一部分,如果嵌入的 struct 有多个字段,我们一般会把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔开来。甚至可以把获取锁、释放锁、计数加一的逻辑封装成一个方法。
版权声明: 本文为 InfoQ 作者【宇宙之一粟】的原创文章。
原文链接:【http://xie.infoq.cn/article/8ac47aa4ecb35465c4375ea7e】。文章转载请联系作者。
评论