Go并发之Mutex

date
Feb 22, 2023
slug
go-mutex
author
status
Public
tags
Blog
Golang
summary
type
Post
thumbnail
category
updatedAt
Mar 15, 2023 09:46 AM

Mutex

Go语言中提供了标准库sync.mutex用来解决高并发中资源访问的问题,本文参考极客时间专栏《Go并发编程实战课》
 

实现机制

说白了就是一个资源在被某个goroutine持有的时候,其他goroutine不能进入临界区,否则就会返回失败,或者等待。那什么是临界区呢?
 
在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。
 

使用方法

 
Mutex实际上就是实现了package sync里的Locker接口
type Locker interface {
    Lock()
    Unlock()
}
可以看到这个接口简单粗暴,就是加锁和解锁,很符合Golang的风格··
 
Mutex的使用实际上也就是这么简单,进入临界区Lock,退出临界区的时候Unlock
func(m *Mutex)Lock() 
func(m *Mutex)Unlock()
 
理解锁的作用之前,先来看看反面教材,当我们用10个goroutine分别给某个变量加10000(一万),正常情况下预期的结果是这个变量最后的值是100000(十万),不加锁的情况下:
package main

import (
	"fmt"
	"sync"
)

func main() {

	count := 0
	wg := sync.WaitGroup{}
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {

			defer wg.Done()

			for j := 0; j < 10000; j++ {
				count++
			}

		}()
	}

	wg.Wait()
	fmt.Println(count)
}
notion image
运行结果不符合预期,因为count++ 不是原子操作,我的理解是虽然每次加的操作都成功了,但是当前的操作只会关注当前goroutine操作前读取到的值,哪怕有其他goruntine同时执行➕操作,当前操作也是感知不到的,这就造成得到的值永远比预期的要小。
 
这个案例是我们故意写出来的,所以错误显而易见。在一个大型的程序中,我们需要排查这种问题的工具:Go race detector,它是基于google的C/C++ sanitizers技术实现的,编译器通过探测所有内存访问,加入代码监视这些内存地址的访问(读/写),在代码运行的时候,能够监视到变量的非同步访问
 

go race detector使用方法

 
go run -race mutex.go
 
notion image
信息很详细,可以准确的定位到data race的问题(生产环境慎用,根据原理可以看出比较影响性能)
 
这里有一点需要注意:Mutex 的零值是还没有 goroutine 等待的未加锁的状态,所以你不需要额外的初始化,直接声明变量(如 var mu sync.Mutex)即可。
 
mutex正是用来解决这种问题,使用方法如下,进入临界区lock,离开时unlock·
····
for j := 0; j < 10000; j++ {
				mu.Lock()
				count++
				mu.Unlock()
			}
····

问题解决

~ go run -race mutex.go
100000
 

如何优雅的使用mutex

 
在一些开源的项目中,我们会看到sync.Mutex会嵌入到struct中使用
type Counter struct{
	mu sync.Mutex // 不用初始化,一般放在控制字段的上面
	Count uint64

	other string
}
通过嵌入字段,可以在struct上直接调用Lock/Unlock方法
func main() {
    var counter Counter
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 100000; j++ {
                counter.Lock()
                counter.Count++
                counter.Unlock()
            }
        }()
    }
    wg.Wait()
    fmt.Println(counter.Count)
}


type Counter struct {
    sync.Mutex
    Count uint64
}
 

拓展

果 Mutex 已经被一个 goroutine 获取了锁,其它等待中的 goroutine 们只能一直等待。那么,等这个锁释放后,等待中的 goroutine 中哪一个会优先获取 Mutex 呢?
 
等待的goroutine们是以FIFO排队的
  1. 当mutex处于正常状态的时候,若此时没有新的goroutine与队头goroutine竞争,则队头goroutine获得,如有,则新的goroutine大概率获得
  1. 当队头goroutine竞争锁失败1ms后,它会将Mutex调整为饥饿模式。进入饥饿模式后,锁的所有权会直接从解锁goroutine移交给队头goroutine,此时新来的goroutine直接放入队尾。
  1. 当一个goroutine获取锁后,如果发现自己满足下列条件中的任何一个#1它是队列中最后一个#2它等待锁的时间少于1ms,则将锁切换回正常模式