当前位置:首页 > 技术知识 > 正文内容

一文弄懂 GO 的 互斥锁 Mutex !(互斥锁的使用方法)

maynowei9个月前 (08-03)技术知识182

在 Go 语言并发编程中,互斥锁(Mutex)是一个非常重要的同步原语。本文将深入介绍 Mutex 的使用方法、实现原理以及最佳实践。

1. 什么是 Mutex?

Mutex(互斥锁)是一种用于多线程编程中防止竞态条件的同步机制。它能够保证在同一时刻只有一个 goroutine 可以访问共享资源,从而避免数据竞争问题。

Go 语言中的 Mutex 定义在 sync 包中:

type Mutex struct {

}
 #技术分享 #掘金

2. Mutex 的基本用法

2.1 简单示例

package main

import ( "fmt" "sync" )

type Counter struct { mu sync.Mutex count int }

func (c *Counter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.count++ }

func main() { counter := Counter{} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment() }() } wg.Wait() fmt.Println("Final count:", counter.count) }

2.2 主要方法

Mutex 提供了两个主要方法:

  • Lock() :获取锁
  • Unlock() :释放锁

3. Mutex 的注意事项

3.1 避免死锁

func (c *Counter) BadPractice() {
    c.mu.Lock()
    c.mu.Lock()
    c.count++

}

func (c *Counter) GoodPractice() { c.mu.Lock() defer c.mu.Unlock() c.count++ }

3.2 锁的粒度

func (c *Counter) CoarseLock() {
    c.mu.Lock()
    defer c.mu.Unlock()

    time.Sleep(time.Second)
    c.count++
}

func (c *Counter) FineLock() { time.Sleep(time.Second) c.mu.Lock() defer c.mu.Unlock() c.count++ }

4. Mutex 的高级特性

4.1 模式切换

sync.Mutex 根据情况在 正常模式饥饿模式 之间进行切换,在并发性能与公平性之间实现动态平衡。

正常模式(Non-Fair Mode,默认模式)

在此模式下,允许新请求的 Goroutine 抢占锁,实现最大化吞吐量。

锁竞争流程

新请求锁的 Goroutine 先尝试通过 CAS 直接抢占锁,若抢占失败,则判断当前是否满足自旋条件(下文会讲到),满足则自旋重试,否则则加入 FIFO 等待队列并阻塞。

当锁释放时,即唤醒队列头部的 GOroutine,同时也允许新请求的 GOroutine 抢占,但是由于新请求已在 CPU 上了,所以往往比刚唤醒的 Goroutine 更容易抢到锁,导致被唤醒的 Goroutine 又继续被阻塞。

性能特点

高吞吐量:新请求 Goroutine 可以插队,可以减少上下文切换;潜在不公平:等待队列中的 Goroutine 有可能一直抢占不到锁,出现饥饿的情况。

饥饿模式(Starvation Mode)

该模式主要是为了解决长时间等待的 Goroutine “饿死” 问题,保证公平性。触发条件

  • 任一 Goroutine 等待时间 >= 1ms
  • 等待队列非空且仅剩一个 Goroutine 时(Go 1.9+ 优化)

当队列仅剩一个 Goroutine 时,表明已处于低竞争状态,此时强制饥饿模式可 加速队列清空 。而竞争只会增加额外的上下文切换和调度开销,而不会提升吞吐量,可能会出现 模式振荡 ,导致频繁的模式切换(如新请求突然涌入又触发饥饿模式),而直接进入饥饿模式可稳定完成最后的锁移交。

锁竞争流程

解锁时直接 将锁交给等待队列头部的 Goroutine ,新请求的 Goroutine 无法参与竞争,而是插入队列尾部。

退出条件

当队列头部的 Goroutine 等待时间 <1ms队列为空 时,切换回正常模式

性能特点

  • 高公平性:先到先得原则
  • 低吞吐:禁止新请求插队,增加上下文切换开销

4.2 自旋机制

即在 Goroutine 获取锁失败时,在满足自旋条件的情况下,允许该 Goroutine 重试上锁,避免短期锁等待导致 Goroutine 阻塞,尽量减少上下文切换开销。

自旋条件

  1. 锁已被占用,并且锁不处于饥饿模式(饥饿模式下禁止自旋)。
  2. 积累的自旋次数小于最大自旋次数(active_spin=4)。
  3. cpu 核数大于 1,单核自旋无意义。
  4. 有空闲的 P。
  5. 调度器空闲 :当前 P(Processor)的本地运行队列为空,且无其他自旋中的 M(Machine Thread)。

4.3 读写锁 (RWMutex)

当读操作远多于写操作时,使用 RWMutex 可以提高并发性能:

type DataStore struct {
    rwmu sync.RWMutex
    data map[string]string
}

func (ds *DataStore) Read(key string) string { ds.rwmu.RLock() defer ds.rwmu.RUnlock() return ds.data[key] }

func (ds *DataStore) Write(key, value string) { ds.rwmu.Lock() defer ds.rwmu.Unlock() ds.data[key] = value }

5. 性能优化建议

  1. 避免锁复制
func copyMutex(m sync.Mutex) { ... }

func copyMutex(m *sync.Mutex) { ... }
  1. 合理使用 defer
func quickLock(c *Counter) {
    c.mu.Lock()
    c.count++

}
  1. 减少锁竞争
type ShardedMap struct {
    shards    [256]struct {
        sync.Mutex
        data map[string]string
    }
}

func (m *ShardedMap) getShardIndex(key string) int { return int(hash(key) % 256) }

6. 常见错误模式

6.1 重复解锁

func (c *Counter) Wrong() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
    c.mu.Unlock()
}

6.2 忘记解锁

func (c *Counter) Wrong() {
    c.mu.Lock()
    if c.count < 0 {
        return
    }
    c.count++

}

func (c *Counter) Correct() { c.mu.Lock() defer c.mu.Unlock() if c.count < 0 { return } c.count++ }

总结

  1. Mutex 是 Go 语言中重要的同步原语,用于保护共享资源。
  2. 正确使用 Mutex 需要注意避免死锁、合理控制锁粒度。
  3. RWMutex 适用于读多写少的场景。
  4. 性能优化时要注意避免锁复制、减少锁竞争。
  5. 使用 defer 确保锁的正确释放。

掌握 Mutex 的正确使用方式对于编写高质量的并发程序至关重要。在实际开发中,要根据具体场景选择合适的同步策略,既要确保程序的正确性,也要兼顾性能。

优质项目推荐

推荐一个可用于练手、毕业设计参考、增加简历亮点的项目。


lemon-puls/txing-oj-backend: Txing 在线编程学习平台,集在线做题、编程竞赛、即时通讯、文章创作、视频教程、技术论坛为一体

相关文章

Android让视图折叠(安卓叠加视图设置)

Android UI Libs之ExpandableLayout1. 说明ExpandableLayout,顾名思义,可扩展的布局,是一个可以帮助我们实现折叠功能的第三方库,折叠时,只显示头部,打开时...

C# 中的多线程同步机制:lock、Monitor 和 Mutex 用法详解

在多线程编程中,线程同步是确保多个线程安全地访问共享资源的关键技术。C# 提供了几种常用的同步机制,其中 lock、Monitor 和 Mutex 是最常用的同步工具。本文将全面介绍这三种同步机制的用...

Qt QWaitCondition 的正确使用方法

简单用法QWaitCondition 用于多线程的同步,一个线程调用QWaitCondition::wait() 阻塞等待,直到另一个线程调用QWaitCondition::wake() 唤醒才继续往...

Oracle标准化部署手册(oracle19c客户端)

很久之前写过一篇11g的windows安装手册, 这次是19c的windows安装手册,面向没有数据库安装部署经验的开发人员或想学习数据库的新手。希望能给想从事dba的入门人员小小的帮助。 毕竟每个高...

一个快要被忘记的数据库开发岗位,但应该被尊重

数据库测试,似乎是被人遗忘的数据库职业,但依然是不错的选择。底下是我在某站找的招聘启事,就连蚂蚁金服都在积极寻找数据库测试人:要说我经历的项目,大大小小也有几十个,从 C/S, B/S, 再到 B/C...