Go 语言快速入门指南: Go 并发互斥锁
互斥是并发编程中最关键的概念之一。当我们使用 goruntine 和channels 进行并发编程时,如果两个 goruntine 尝试同时访问同一个内存位置的同一数据会发生竞争,有时候会产生意想不到的结果,通常很难调试,不符合日常要求,出现错误甚至很难修复。
生活场景
假设在生活中可能会发生的例子:有一个银行系统,我们可以从银行余额中存款和取款。在一个单线程的同步程序中,这个操作很简单。我们可以通过少量的单元测试有效地保证它每次都能按计划工作。
然而,如果我们开始引入多个线程,在 Go 语言中使用多个 goroutine,我们可能会开始在我们的代码中看到问题。
互斥锁和读写锁
互斥锁,英文名 Mutex,顾名思义,就是相互排斥,是保护程序中临界区的一种方式。而临界区是程序中需要独占访问共享资源的区域。互斥锁提供了一种安全的方式来表示对这些共享资源的独占访问。为了使用资源,channel 通过通信共享内存,而 Mutex 通过开发人员的约定同步访问共享内存。
让我们看一个没有 Mutex 的并发编程的代码:
package main
import (
"fmt"
"sync"
)
type calculation struct {
sum int
}
func main() {
test := calculation{}
test.sum = 0
wg := sync.WaitGroup{}
for i := 0; i < 500; i++ {
wg.Add(1)
go dosomething(&test, &wg)
}
wg.Wait()
fmt.Println(test.sum)
}
func dosomething(test *calculation, wg *sync.WaitGroup) {
test.sum++
wg.Done()
}
第一次结果为:491
第二次结果:493
在上面的例子中,我们声明了一个名为 test 的计算结构体,并通过 for 循环产生了多个 GoRoutines,将 sum 的值加 1。(如果你对 GoRoutines 和 WaitGroup 不熟悉,请参考之前的教程)。 我们可能期望 for 循环后 sum 的值应该是 500。然而,这可能不是真的。 有时,您可能会得到小于 500(当然永远不会超过 500)的结果。 这背后的原因是两个 GoRoutine 有一定的概率在相同的内存位置操作相同的变量,从而导致这种意外结果。 这个问题的解决方案是使用互斥锁。
使用 Mutex
package main
import (
"fmt"
"sync"
)
type calculation struct {
sum int
mutex sync.Mutex
}
func main() {
test := calculation{}
test.sum = 0
wg := sync.WaitGroup{}
for i := 0; i < 500; i++ {
wg.Add(1)
go dosomething(&test, &wg)
}
wg.Wait()
fmt.Println(test.sum)
}
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()
err1 :=...
if err1 != nil {
return err1
}
err2 :=...
if err2 != nil {
return err2
}
// ... do more stuff ...
return nil
}
}在这种情况下,我们有多个 if err!=nil 这可能会导致函数提前退出。 通过使用 defer,无论函数如何返回,我们都可以保证释放锁。 否则,我们需要将 Unlock() 放在函数可能返回的每个地方。 然而,这并不意味着我们应该一直使用 defer。 让我们再看一个例子。
func dosomething(test *calculation){
test.mutex.Lock()
defer test.mutex.Unlock()
// modify the variable which requires mutex protect
test.sum =...
// perform a time consuming IO operation
http.Get()
}
在这个例子中,mutex 不会释放锁,直到耗时的函数(这里是 http.Get())完成。 在这种情况下,我们可以在 test.sum=... 行之后解锁互斥锁,因为这是我们操作变量的唯一地方。
总结
很多时候 Mutex 并不是单独使用的,而是嵌套在 Struct 中使用,作为结构体的一部分,如果嵌入的 struct 有多个字段,我们一般会把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔开来。甚至可以把获取锁、释放锁、计数加一的逻辑封装成一个方法。