深入理解 go Mutex

本文深入探讨 Go 语言中的互斥锁(Mutex),从基本用法到实现原理,包括信号量、等待队列、原子操作,以及公平性和源码分析。通过示例介绍了 Gin Context 和 sync.Pool 中 Mutex 的应用,并提醒了使用 Mutex 的注意事项,帮助开发者更好地理解和使用 Go 中的并发控制机制。
摘要由CSDN通过智能技术生成

在我们的日常开发中,总会有时候需要对一些变量做并发读写,比如 web 应用在同时接到多个请求之后, 需要对一些资源做初始化,而这些资源可能是只需要初始化一次的,而不是每一个 http 请求都初始化, 在这种情况下,我们需要限制只能一个协程来做初始化的操作,比如初始化数据库连接等, 这个时候,我们就需要有一种机制,可以限制只有一个协程来执行这些初始化的代码。 在 go 语言中,我们可以使用互斥锁(Mutex)来实现这种功能。

互斥锁的定义

这里引用一下维基百科的定义:

互斥锁(Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两个线程同时对同一公共资源 (比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。 临街区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

互斥,顾名思义,也就是只有一个线程能持有锁。当然,在 go 中,是只有一个协程能持有锁。

下面是一个简单的例子:

var sum int // 和
var mu sync.Mutex // 互斥锁

// add 将 sum 加 1
func add() {
    // 获取锁,只能有一个协程获取到锁,
    // 其他协程需要阻塞等待锁释放才能获取到锁。
   mu.Lock()
   // 临界区域
   sum++
   mu.Unlock()
}

func TestMutex(t *testing.T) {
   // 启动 1000 个协程
   var wg sync.WaitGroup
   wg.Add(1000)

   for i := 0; i < 1000; i++ {
      go func() {
         // 每个协程里面调用 add()
         add()
         wg.Done()
      }()
   }

   // 等待所有协程执行完毕
   wg.Wait()
   // 最终 sum 的值应该是 1000
   assert.Equal(t, 1000, sum)
}
复制代码

上面的例子中,我们定义了一个全局变量 sum,用于存储和,然后定义了一个互斥锁 mu, 在 add() 函数中,我们使用 mu.Lock() 来加锁,然后对 sum 进行加 1 操作, 最后使用 mu.Unlock() 来解锁,这样就保证了在任意时刻,只有一个协程能够对 sum 进行加 1 操作, 从而保证了在并发执行 add() 操作的时候 sum 的值是正确的。

上面这个例子,在我之前的文章中已经作为例子出现过很多次了,这里不再赘述了。

go Mutex 的基本用法

Mutex 我们一般只会用到它的两个方法:

  • Lock:获取互斥锁。(只会有一个协程可以获取到锁,通常用在临界区开始的地方。)
  • Unlock: 释放互斥锁。(释放获取到的锁,通常用在临界区结束的地方。)

Mutex 的模型可以用下图表示:

说明:

  • 同一时刻只能有一个协程获取到 Mutex 的使用权,其他协程需要排队等待(也就是上图的 G1->G2->Gn)。
  • 拥有锁的协程从临界区退出的时候需要使用 Unlock 来释放锁,这个时候等待队列的下一个协程可以获取到锁(实际实现比这里说的复杂很多,后面会细说),从而进入临界区。
  • 等待的协程会在 Lock 调用处阻塞,Unlock 的时候会使得一个等待的协程解除阻塞的状态,得以继续执行。

上面提到的这几点也是 Mutex 的基本原理。

互斥锁使用的两个例子

了解了 go Mutex 基本原理之后,让我们再来看看 Mutex 的一些使用的例子。

gin Context 中的 Set 方法

一个很常见的场景就是,并发对 map 进行读写,熟悉 go 的朋友应该知道,go 中的 map 是不支持并发读写的, 如果我们对 map 进行并发读写会导致 panic

而在 gin 的 Context 结构体中,也有一个 map 类型的字段 Keys,用来在上下文间传递键值对数据, 所以在通过 Set 来设置键值对的时候需要使用 c.mu.Lock() 来先获取互斥锁,然后再对 Keys 做设置。

// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes  c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) {
    // 获取锁
   c.mu.Lock()
    // 如果 Keys 还没初始化,则进行初始化
   if c.Keys == nil {
      c.Keys = make(map[string]any)
   }

    // 设置键值对
   c.Keys[key] = value
    // 释放锁
   c.mu.Unlock()
}
复制代码

同样的,对 Keys 做读操作的时候也需要使用互斥锁:

// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
    // 获取锁
   c.mu.RLock()
    // 读取 key
   value, exists = c.Keys[key]
    // 释放锁
   c.mu.RUnlock()
   return
}
复制代码

可能会有人觉得奇怪,为什么从 map 中读也还需要锁。这是因为,如果读的时候没有锁保护, 那么就有可能在 Set 设置的过程中,同时也在进行读操作,这样就会 panic 了。

这个例子想要说明的是,像 map 这种数据结构本身就不支持并发读写,我们这种情况下只有使用 Mutex 了。

sync.Pool 中的 pinSlow 方法

在 sync.Pool 的实现中,有一个全局变量记录了进程内所有的 sync.Pool 对象,那就是 allPools 变量, 另外有一个锁 allPoolsMu 用来保护对 allPools 的读写操作:

var (
   // 保护 allPools 和 oldPools 的互斥锁。
   allPoolsMu Mutex

   // allPools is the set of pools that have non-empty primary
   // caches. Protected by either 1) allPoolsMu and pinning or 2)
   // STW.
   allPools []*Pool

   // oldPools is the set of pools that may have non-empty victim
   // caches. Protected by STW.
   oldPools []*Pool
)
复制代码

pinSlow 方法中会在 allPoolsMu 的保护下对 allPools 做读写操作:

func (p *Pool) pinSlow() (*poolLocal, int) {
   // Retry under the mutex.
   // Can not lock the mutex while pinned.
   runtime_procUnpin()
   allPoolsMu.Lock() // 获取锁
   defer allPoolsMu.Unlock() // 函数返回的时候释放锁
   pid := runtime_procPin()
   // poolCleanup won't be called while we are pinned.
   s := p.localSize
   l := p.local
   if uintptr(pid) < s {
      return indexLocal(l, pid), pid
   }
   if p.local == nil {
      allPools = append(allPools, p) // 全局变量修改
   }
   // If GOMAXPROCS changes between GCs, we re-allocate the array and lose the old one.
   size := runtime.GOMAXPROCS(0)
   local := make([]poolLocal, size)
   atomic.Stor
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值