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

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.dtcms.com/a/335955.html

相关文章:

  • 深入理解 uni-app 页面导航:switchTab、navigateTo、redirectTo、reLaunch 与 navigateBack
  • 2.4 双向链表
  • QUIC浅析
  • 流浪循环 全DLC(Rogue Loops)免安装中文版
  • 超市电商销售分析项目:从数据分析到业务决策
  • 【架构师从入门到进阶】第五章:DNSCDN网关优化思路——第十一节:网关安全-对称与非对称加密
  • PHP静态类self和static用法
  • 【计算机视觉与深度学习实战】04基于K-Means聚类的图像分割系统设计与实现
  • Java Stream 初解
  • 14.web api 5
  • 基于MATLAB多智能体强化学习的出租车资源配置优化系统设计与实现
  • 无人机视角乱堆垃圾垃圾场地分割数据集labelme格式1501张1类别
  • qt svg缺失元素, 原因是不支持 rgba
  • Android studio gradle有关设置
  • 图解 setTimeout + 循环:var 共享变量 vs let 独立绑定
  • 《若依》介绍和环境搭建
  • 基于径向基函数神经网络的数据回归预测 RBF
  • 2024年08月13日 Go生态洞察:Go 1.23 发布与全面深度解读
  • 三维重建-动手学计算机视觉19(完结)
  • Android Studio中创建Git分支
  • ——分治——
  • metasploit 框架安装更新遇到无法下载问题如何解决
  • Sentinel和12.5米高程的QGIS 3D效果
  • 双椒派E2000D Sysfs与GPIO控制实战指南
  • KINGBASE集群日常维护管理命令总结
  • 云原生俱乐部-杂谈3
  • 深入掌握 Kubernetes Deployment:部署、重启、管理和维护全攻略
  • 为什么TCP连接是三次握手?不是四次两次?
  • 《Cocos游戏开发入门一本通》第四章
  • 智能体的记忆(Memory)系统