当前位置: 首页 > backend >正文

Go语言并发编程 ------ 锁机制详解

Go语言提供了丰富的同步原语来处理并发编程中的共享资源访问问题。其中最基础也最常用的就是互斥锁(Mutex)和读写锁(RWMutex)。

1. sync.Mutex(互斥锁)

Mutex核心特性

  • 互斥性/排他性同一时刻只有一个goroutine能持有锁
  • 不可重入:同一个goroutine重复加锁会导致死锁
  • 零值可用sync.Mutex的零值就是未锁定的互斥锁
  • 非公平锁:不保证goroutine获取锁的顺序

Mutex例子

例1:

package mainimport ("fmt""math/rand""sync""time"
)var wait sync.WaitGroup
var count = 0var lock sync.Mutexfunc main() {wait.Add(10)for i := 0; i < 10; i++ {go func(data *int) {// 加锁lock.Lock()// 模拟访问耗时time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))// 访问数据temp := *data// 模拟计算耗时time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))ans := 1// 修改数据*data = temp + ans// 解锁lock.Unlock()fmt.Println(*data)wait.Done()}(&count)}wait.Wait()fmt.Println("最终结果", count)
}

输出:

1
2
3
4
5
6
7
8
9
10
最终结果 10

解读:

  • lock 是一个互斥锁,用于确保在任何时刻只有一个 goroutine 可以访问和修改 count 变量,防止数据竞争。
  • 每个 goroutine 首先通过 lock.Lock() 加锁,确保在同一时间只有一个 goroutine 可以修改 count。
  • time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) 模拟了对数据的访问和计算耗时,这里的随机数生成器用于在每次循环中生成一个 0 到 999 之间的随机整数,作为睡眠的时间
  • wait.Wait() 阻塞主 goroutine,直到等待组中的所有 goroutine 都完成任务。
  • fmt.Println("最终结果", count) 打印 count 的最终值。

在 Go 语言中,func(data *int) 这样的写法是用来定义一个匿名函数,并且该匿名函数接受一个参数,参数类型是指向整型的指针。在这段代码的目的是在并发环境中对一个共享变量 count 进行修改,以避免数据竞争。

  • go func(data *int) { ... }(&count) 这里的 go 关键字用于启动一个新的 goroutine。
  • func(data *int) { ... } 是一个匿名函数,它接受一个参数 data,这个参数是一个指向整型的指针。
  • (&count) 表示传递给匿名函数的参数是 count 变量的地址。通过传递指针,匿名函数可以直接访问和修改 count 的值。

例2

package mainimport ("fmt""sync""time"
)var (counter intlock    sync.Mutex
)func main() {var wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go increment(&wg)}wg.Wait()fmt.Println("Final counter:", counter)
}func increment(wg *sync.WaitGroup) {defer wg.Done()lock.Lock()         // 加锁defer lock.Unlock() // 使用defer确保解锁// 临界区temp := countertime.Sleep(1 * time.Millisecond)counter = temp + 1
}

输出:

Final counter: 10

解读:

  • var wg sync.WaitGroup:声明了一个等待组wg,用于等待所有goroutine完成。
  • wg.Add(1):为每次循环增加一个等待计数。
  • go increment(&wg):启动goroutine运行increment函数,并传入等待组的地址。
  • wg.Wait():等待所有等待计数为零,即所有goroutine完成。
  • defer wg.Done():使用defer关键字确保函数执行完毕后调用wg.Done(),减少等待组的一个计数。
  • lock.Lock():在函数执行前加锁,防止多个goroutine同时访问counter。
  • defer lock.Unlock():同样使用defer关键字确保函数执行完毕后解锁。
  • 临界区代码段:将counter的值赋给temp,休眠1毫秒,然后将counter设置为temp + 1。这里通过休眠模拟了一个耗时操作。

2. sync.RWMutex(读写锁)

Go 中读写互斥锁的实现是 sync.RWMutex,它也同样实现了 Locker 接口,但它提供了更多可用的方法,如下:

// 加读锁
func (rw *RWMutex) RLock()// 非阻塞地尝试加读锁 (Go 1.18+)
func (rw *RWMutex) TryRLock() bool// 解读锁
func (rw *RWMutex) RUnlock()// 加写锁
func (rw *RWMutex) Lock()// 非阻塞地尝试加写锁 (Go 1.18+)
func (rw *RWMutex) TryLock() bool// 解写锁
func (rw *RWMutex) Unlock()

1. RWMutex基本概念

读写锁的特点

  • 并发读:多个goroutine可以同时持有读锁
  • 互斥写:写锁是排他的,同一时间只能有一个goroutine持有写锁
  • 写优先:当有写锁等待时,新的读锁请求会被阻塞,防止写锁饥饿

Mutex的区别

特性MutexRWMutex
并发读不支持支持多个goroutine同时读
并发写不支持不支持
性能一般读多写少场景性能更好
复杂度简单相对复杂

2. RWMutex的工作原理

锁状态

  • 当写锁被持有时:所有读锁和写锁请求都会被阻塞
  • 当读锁被持有时:新的读锁请求可以立即获得锁,写锁请求会被阻塞
  • 当写锁请求等待时:新的读锁请求会被阻塞(写优先)

内部实现要点

  1. 读者计数:记录当前持有读锁的goroutine数量
  2. 写者标记:标识是否有goroutine持有或等待写锁
  3. 写者信号量:用于唤醒等待的写者
  4. 读者信号量:用于唤醒等待的读者

3. RWMutex的例子

线程安全的缓存实现

type Cache struct {mu    sync.RWMutexitems map[string]interface{}
}func (c *Cache) Get(key string) (interface{}, bool) {c.mu.RLock()defer c.mu.RUnlock()item, found := c.items[key]return item, found
}func (c *Cache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()c.items[key] = value
}func (c *Cache) Delete(key string) {c.mu.Lock()defer c.mu.Unlock()delete(c.items, key)
}

解读

Cache 结构体

  • items:一个映射(map),键为字符串,值为接口类型(interface{}),用于存储缓存数据。
  • mu:一个sync.RWMutex实例,用于控制对items的并发访问。

Get 方法:

  • c.mu.RLock():获取读锁,允许多个读协程同时访问items。
  • defer c.mu.RUnlock():确保在函数返回前释放读锁
  • item, found := c.items[key]:从items中获取指定key对应的值,并判断该key是否存在。
  • return item, found:返回获取的值和是否找到的布尔值。

Set 方法:

  • c.mu.Lock():获取写锁,确保只有一个写协程可以访问items。
  • defer c.mu.Unlock():确保在函数返回前释放写锁
  • c.items[key] = value:将指定key对应的值设置为value。

Delete 方法:

  • c.mu.Lock():获取写锁,确保只有一个写协程可以访问items。
  • defer c.mu.Unlock():确保在函数返回前释放写锁。
  • delete(c.items, key):从items中删除指定key对应的键值对。

3.互斥锁和读写锁的区别和应用场景

核心区别对比

特性互斥锁(Mutex)读写锁(RWMutex)
并发读完全互斥,读操作也需要独占锁允许多个goroutine同时持有读锁
并发写互斥,同一时间只有一个写操作互斥,同一时间只有一个写操作
锁类型单一锁类型区分读锁(RLock)和写锁(Lock)
性能开销较高(所有操作都互斥)读操作开销低,写操作开销与Mutex相当
实现复杂度简单相对复杂
适用场景读写操作频率相当或写多读少读操作远多于写操作的场景

选择场景

  1. 优先考虑RWMutex当

    • 读操作次数是写操作的5倍以上
    • 读操作临界区较大(耗时较长)
    • 需要支持高频并发读取
  2. 选择Mutex当

    • 读写操作频率相当(写操作占比超过20%)
    • 临界区非常小(几个CPU周期就能完成)
    • 代码简单性比极致性能更重要
    • 需要锁升级/降级(虽然Go不支持,但Mutex更不容易出错)
  3. 特殊考虑

    • 对于极高性能场景,可考虑atomic原子操作
    • 对于复杂场景,可考虑sync.Map或分片锁
http://www.xdnf.cn/news/18045.html

相关文章:

  • 【C++知识杂记1】智能指针及其分类
  • w嵌入式分享合集68
  • 什么是EDA(Exploratory Data Analysis,探索性数据分析)
  • MariaDB 多源复制
  • Windchill 11 Enumerated Type Customization Utility-枚举类型自定义实用程序
  • 嵌入式开发入门—电子元器件~半导体
  • Linux设备模型深度解析
  • 运动场和光流-动手学计算机视觉17
  • Spring 源码学习(十一)—— webmvc 配置
  • 【k8s、docker】Headless Service(无头服务)
  • Tomcat Connector连接器原理
  • 阶段二:7-上网行为安全概述
  • Spring Boot 项目配置 MySQL SSL 加密访问
  • SQL详细语法教程(四)约束和多表查询
  • 智能汽车领域研发,复用云原始开发范式?
  • 开源数据发现平台:Amundsen Search Service 搜索服务
  • SparkSQL性能优化实践指南
  • gRPC网络模型详解
  • 从0开始学习Java+AI知识点总结-17.web基础知识(数据库)
  • ARM汇编代码新手入门
  • 【人工智能99问】残差链接是什么,是如何起作用的?(28/99)
  • C语言相关简单数据结构:双向链表
  • 影刀 RAP 迁移华为云备忘录数据到得到笔记
  • C++编程实战:高效解决算法与数据结构问题
  • Python多线程、锁、多进程、异步编程
  • 自动驾驶中的传感器技术34——Lidar(9)
  • Python训练营打卡Day35-复习日
  • 2025年5月架构设计师综合知识真题回顾,附参考答案、解析及所涉知识点(五)
  • Pandas 和 NumPy的区别和联系
  • 安卓开发中遇到Medium Phone API 36.0 is already running as process XXX.