7天用Go从零实现分布式缓存GeeCache(学习)(4)上锁总结

Go 语言锁机制与代码中加锁的详细说明

Go 语言提供了多种锁机制来控制并发访问,以确保数据的一致性和安全性。本文将首先介绍 Go 中常用的几种锁机制,然后详细说明您的代码中加锁的地方、加锁的目的,以及锁是如何确保线程安全的。最后,将这两部分内容进行整理和合并,帮助您更全面地理解 Go 语言的并发控制和代码实现。


一、Go 语言中的锁机制

Go 语言的 sync 包提供了多种用于并发控制的锁机制,以下是常用的几种:

1. sync.Mutex(互斥锁)

  • 用途:控制对共享资源的独占访问,只允许一个 goroutine 持有锁,防止数据竞争(race condition)。
  • 使用方法:调用 Lock() 加锁,Unlock() 解锁。
  • 适用场景:适用于需要完全互斥的场景,例如对共享变量的写操作。

示例

var mu sync.Mutexfunc increment() {mu.Lock()counter++mu.Unlock()
}

2. sync.RWMutex(读写锁)

  • 用途:允许多个 goroutine 同时读取,但写操作是独占的,能提高读多写少场景的性能。
  • 使用方法
    • RLock() 获取读锁,RUnlock() 释放读锁;
    • Lock() 获取写锁,Unlock() 释放写锁。
  • 适用场景:适用于读多写少的场景,例如缓存、配置文件等。

示例

var mu sync.RWMutexfunc read() {mu.RLock()defer mu.RUnlock()fmt.Println(counter)
}func write() {mu.Lock()defer mu.Unlock()counter++
}

3. sync.Once(单次锁)

  • 用途:确保某些初始化操作只执行一次。
  • 使用方法:调用 Do(func),传入的函数只会执行一次,无论多少 goroutine 调用 Do
  • 适用场景:适用于单次初始化的场景,例如单例模式或仅初始化一次的资源。

示例

var once sync.Oncefunc initialize() {once.Do(func() {fmt.Println("Initializing...")})
}

4. sync.Cond(条件变量)

  • 用途:用于协调多个 goroutine 的等待和通知操作,通常配合 Mutex 使用。
  • 使用方法
    • Wait() 等待条件满足并自动释放锁;
    • Signal() 唤醒一个等待的 goroutine;
    • Broadcast() 唤醒所有等待的 goroutine。
  • 适用场景:适用于需要等待条件的场景,例如生产者-消费者模型。

示例

var mu sync.Mutex
var cond = sync.NewCond(&mu)func waitForCondition() {cond.L.Lock()cond.Wait()  // 等待通知fmt.Println("Condition met")cond.L.Unlock()
}func signalCondition() {cond.L.Lock()cond.Signal()  // 通知一个等待中的 goroutinecond.L.Unlock()
}

5. sync.Map(并发安全的 Map)

  • 用途:实现并发安全的键值对存储,适合高并发环境下的读写操作。
  • 使用方法:提供 StoreLoadDeleteRange 等方法来操作 map。
  • 适用场景:适用于大量的读写操作,例如缓存。

示例

var m sync.Mapfunc main() {m.Store("key", "value")if v, ok := m.Load("key"); ok {fmt.Println(v)}m.Delete("key")
}

6. sync.WaitGroup(等待组)

  • 用途:用于等待一组并发操作完成。
  • 使用方法
    • Add(n) 添加要等待的 goroutine 数量;
    • Done() 表示某个 goroutine 完成;
    • Wait() 阻塞直到所有 goroutine 完成。
  • 适用场景:适用于需要等待多个 goroutine 完成的场景,例如并发任务的汇总。

示例

var wg sync.WaitGroupfunc worker() {defer wg.Done()fmt.Println("Working...")
}func main() {wg.Add(2)go worker()go worker()wg.Wait()  // 等待所有 worker 完成
}

7. sync/atomic 包(原子操作)

  • 用途:提供底层的原子操作,避免使用锁,但依赖于硬件的原子指令。
  • 使用方法atomic.AddInt32atomic.LoadInt32atomic.StoreInt32 等。
  • 适用场景:适合简单计数、状态切换等轻量级并发场景,避免锁开销。

示例

import "sync/atomic"var counter int32func increment() {atomic.AddInt32(&counter, 1)
}

二、代码中加锁的地方详细说明

1. cache.go 中的加锁

1.1 cache 结构体中的互斥锁

文件geecache/cache.go

type cache struct {mu         sync.Mutex // 互斥锁,保护 lru 和 cacheBytes 的并发访问lru        *lru.Cache // LRU 缓存实例cacheBytes int64      // 缓存的最大字节数
}
  • 加锁目的:保护 lru 缓存和 cacheBytes 字段的并发访问,确保在多协程环境下对缓存的操作是安全的。
1.2 cache 的方法中使用锁
1.2.1 add 方法
func (c *cache) add(key string, value ByteView) {c.mu.Lock()         // 加锁,保护对 lru 的并发访问defer c.mu.Unlock() // 函数退出时解锁// 延迟初始化 lru 缓存if c.lru == nil {c.lru = lru.New(c.cacheBytes, nil)}c.lru.Add(key, value) // 向 lru 缓存中添加数据
}
  • 加锁位置:在方法开始时调用 c.mu.Lock(),在方法结束时使用 defer c.mu.Unlock() 解锁。
  • 加锁目的
    • 确保对 lru 缓存的访问是线程安全的。
    • 防止多个协程同时初始化 lru,导致竞态条件(race condition)。
    • 保护 lru.Add 的调用,因为 lru.Cache 本身不支持并发访问。
1.2.2 get 方法
func (c *cache) get(key string) (value ByteView, ok bool) {c.mu.Lock()         // 加锁,保护对 lru 的并发访问defer c.mu.Unlock() // 函数退出时解锁if c.lru == nil {return}if v, ok := c.lru.Get(key); ok {return v.(ByteView), ok}return
}
  • 加锁位置:同样在方法开始时加锁,结束时解锁。
  • 加锁目的
    • 保护对 lru 缓存的读取操作。
    • 防止并发情况下多个协程同时访问未初始化的 lru,导致空指针异常或其他竞态问题。
    • 确保 lru.Get 的操作是线程安全的。
1.3 lru.Cache 不支持并发访问
  • 原因lru.Cache 内部使用了 Go 标准库的 container/list,该数据结构不是并发安全的。
  • 解决方案:在 cache 结构体中使用互斥锁 mu 来保护对 lru 的所有访问,包括读和写操作。

2. singleflight.go 中的加锁

2.1 Group 结构体中的互斥锁

文件geecache/singleflight/singleflight.go

type Group struct {mu sync.Mutex       // 互斥锁,保护 m 的并发访问m  map[string]*call // 存储正在进行的请求
}
  • 加锁目的:保护 m 字典的并发访问,防止多个协程同时对其进行读写操作。
2.2 Group 的方法中使用锁
2.2.1 Do 方法
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {g.mu.Lock()if g.m == nil {g.m = make(map[string]*call)}if c, ok := g.m[key]; ok {g.mu.Unlock()c.wg.Wait()return c.val, c.err}c := new(call)c.wg.Add(1)g.m[key] = cg.mu.Unlock()c.val, c.err = fn()c.wg.Done()g.mu.Lock()delete(g.m, key)g.mu.Unlock()return c.val, c.err
}
  • 加锁位置

    • 第一次加锁:方法开始时,保护对 g.m 的访问,包括初始化和查找。
    • 第一次解锁:如果发现已有相同的请求在进行中,立即解锁,等待已有请求完成。
    • 第二次加锁:在请求完成后,再次加锁,删除 g.m 中对应的 key
    • 第二次解锁:删除后立即解锁。
  • 加锁目的

    • 第一次加锁
      • 确保对 g.m 的初始化和查找是线程安全的。
      • 防止多个协程同时对 g.m 进行修改,导致竞态条件。
    • 第二次加锁
      • 确保从 g.m 中删除已完成的请求时,不会与其他协程发生冲突。
  • 锁的粒度控制

    • 在持有锁期间,只执行必要的操作,尽快解锁,减少锁的持有时间,提高并发性能。
2.3 等待组 sync.WaitGroup
  • 用途:使用 c.wgsync.WaitGroup)让等待的协程阻塞,直到请求完成,避免重复请求。
  • 与锁的配合
    • 锁用于保护对共享资源 g.m 的访问。
    • WaitGroup 用于协调请求的执行和等待,避免协程忙等或重复执行。

3. http.go 中的加锁

3.1 HTTPPool 结构体中的互斥锁

文件geecache/http.go

type HTTPPool struct {self        string                 // 当前节点的地址basePath    string                 // HTTP 请求的基础路径mu          sync.Mutex             // 互斥锁,保护 peers 和 httpGetters 的并发访问peers       *consistenthash.Map    // 一致性哈希环httpGetters map[string]*httpGetter // 远程节点的 HTTP 客户端映射
}
  • 加锁目的:保护 peershttpGetters 的并发访问,确保节点列表的更新和读取是线程安全的。
3.2 HTTPPool 的方法中使用锁
3.2.1 Set 方法
func (p *HTTPPool) Set(peers ...string) {p.mu.Lock()defer p.mu.Unlock()p.peers = consistenthash.New(defaultReplicas, nil)p.peers.Add(peers...)p.httpGetters = make(map[string]*httpGetter, len(peers))for _, peer := range peers {p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath}}
}
  • 加锁位置:方法开始时加锁,方法结束时通过 defer 解锁。
  • 加锁目的
    • 确保对 peershttpGetters 的更新是原子性的,防止在更新过程中其他协程读取到不完整的数据。
    • 防止多个协程同时调用 Set 方法,导致数据竞争。
3.2.2 PickPeer 方法
func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) {p.mu.Lock()defer p.mu.Unlock()if peer := p.peers.Get(key); peer != "" && peer != p.self {p.Log("Pick peer %s", peer)return p.httpGetters[peer], true}return nil, false
}
  • 加锁位置:方法开始时加锁,方法结束时通过 defer 解锁。
  • 加锁目的
    • 保护对 peershttpGetters 的并发访问,确保读取到的节点信息是一致的。
    • 防止在读取节点信息时,另一个协程正在修改节点列表,导致数据不一致或程序崩溃。
3.3 consistenthash.Map 不支持并发访问
  • 原因consistenthash.Map 没有内部的并发控制机制,直接对其进行并发访问可能导致竞态条件。
  • 解决方案:在 HTTPPool 中使用互斥锁 mu 来保护对 consistenthash.Map 的访问。

4. 其他需要注意的地方

4.1 Group 结构体中的并发控制

文件geecache/geecache.go

type Group struct {name      string              // 缓存组的名称getter    Getter              // 加载数据的回调接口mainCache cache               // 并发安全的本地缓存peers     PeerPicker          // 节点选择器loader    *singleflight.Group // 防止缓存击穿的请求合并
}
  • 并发控制方式
    • 对于本地缓存 mainCache,通过其内部的互斥锁 mu 进行保护。
    • 对于防止缓存击穿,使用了 singleflight.Group,避免并发情况下的重复请求。
    • Group 本身没有使用互斥锁,因为其内部的关键部分都已经由各自的组件进行了并发控制。
4.2 lru.Cache 的并发访问
  • 说明lru.Cache 本身不支持并发访问,需要由外部进行并发控制。
  • 解决方案:在 cache 结构体中使用互斥锁 mu 保护对 lru.Cache 的访问。
4.3 consistenthash.Map 的并发访问
  • 说明consistenthash.Map 没有内部的并发控制,需要在外部加锁。
  • 解决方案:在使用 consistenthash.Map 的地方,如 HTTPPool 中,使用互斥锁 mu 保护对其的访问。

5. 加锁机制如何确保线程安全

5.1 互斥锁 sync.Mutex
  • 工作原理:互斥锁是一种用于保护共享资源的锁机制。在同一时刻,只有一个 goroutine 能够获得互斥锁,从而独占地访问被保护的资源。
  • 在代码中的作用
    • 防止竞态条件:当多个协程同时读写共享资源时,可能会发生竞态条件,导致数据不一致或程序崩溃。通过互斥锁,可以确保共享资源的访问是互斥的,防止竞态发生。
    • 保护临界区:临界区是指对共享资源进行访问的代码片段。在进入临界区之前加锁,退出时解锁,确保临界区内的代码不会被多个协程同时执行。
5.2 加锁的粒度控制
  • 细粒度加锁:在代码中,尽量缩小锁的持有时间,只在需要保护的代码段内持有锁,其他时间尽快解锁,提高并发性能。
  • 避免死锁:在加锁的过程中,注意锁的顺序,避免在多个锁之间形成循环等待,导致死锁。

三、整理和合并

结合以上内容,我们可以总结如下:

  1. Go 语言提供了多种锁机制,包括 sync.Mutexsync.RWMutexsync.Oncesync.Condsync.Mapsync.WaitGroupsync/atomic 包等,用于不同的并发场景。

  2. 在您的代码中,主要使用了 sync.Mutexsync.WaitGroup

    • sync.Mutex 用于保护共享资源的访问,防止竞态条件。
    • sync.WaitGroup 用于等待并发操作完成,防止重复请求。
  3. 代码中加锁的地方

    • cache.go 中的 cache 结构体:使用 sync.Mutex 保护对 lru.Cache 的并发访问,因为 lru.Cache 本身不支持并发。
    • singleflight.go 中的 Group 结构体:使用 sync.Mutexsync.WaitGroup 防止并发情况下的重复请求。
    • http.go 中的 HTTPPool 结构体:使用 sync.Mutex 保护对节点列表和客户端映射的并发访问。
  4. 锁的使用方式和目的

    • 互斥锁 sync.Mutex:在需要对共享资源进行读写操作的地方加锁,确保线程安全。
    • 等待组 sync.WaitGroup:在需要等待多个协程完成操作的地方使用,避免重复请求或提前退出。
  5. 加锁机制确保线程安全的方式

    • 防止竞态条件:通过锁机制,确保同一时间只有一个协程访问共享资源,防止数据不一致。
    • 提高并发性能:通过细粒度加锁,减少锁的持有时间,避免不必要的阻塞。
  6. 整体策略

    • 在需要保护共享资源的地方使用锁,例如对缓存、节点列表等的访问。
    • 在持有锁期间,只执行必要的操作,尽快解锁,提高系统的并发性能。
    • 合理选择锁的类型,根据场景选择合适的锁机制,例如读多写少的场景可以考虑使用 sync.RWMutex

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/14117.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

公链数字钱包开发与加密钱包App原生开发

随着区块链技术的不断发展,数字货币和去中心化金融(DeFi)的兴起,公链数字钱包的需求日益增加。数字钱包不仅为用户提供存储、管理和交易数字资产的工具,而且也为区块链技术的应用提供了一个重要的入口。开发一个安全、…

0. 0:《跟着小王学Python·新手》

《跟着小王学Python新手》系列 《跟着小王学Python》 是一套精心设计的Python学习教程,适合各个层次的学习者。本教程从基础语法入手,逐步深入到高级应用,以实例驱动的方式,帮助学习者逐步掌握Python的核心概念。通过开发游戏、构…

HTTPTomcatServle之HTTP详解

✨博客主页: https://blog.csdn.net/m0_63815035?typeblog 💗《博客内容》:.NET、Java.测试开发、Python、Android、Go、Node、Android前端小程序等相关领域知识 📢博客专栏: https://blog.csdn.net/m0_63815035/cat…

「数据要素」行业简报|2024.11.上刊

纵观数据要素行业动态,洞察行业风向,把握行业脉搏! 一、政策发布 1、《山东省公共数据资源登记管理工作规范(试行)》公开征求意见 11月7日,为认真贯彻落实《中共中央办公厅 国务院办公厅关于加快公共数据资源开发利用的意见》《…

NFS Write IO 不对齐深度分析

背景 最近团队小伙伴弗曼统计了线上用户数据写入对齐情况,通过统计数据发现了一个有趣的现象: 用户写入请求中近 70% 的数据块 4K 不对齐,这也就是说 NFSClient 对大多数的应用写入没有做对齐优化。 下面会从 NFSClient BufferWrite 实现流程的维度解释…

微型导轨在自动化生产线中起什么作用?

在现代制造业的飞速跃进中,自动化生产线的蓬勃发展引领了一场效率与质量的双重革命。微型导轨作为传动领域的重要零部件,可用于工业自动化生产线上的零件运输、加工设备定位等,实现自动化生产和减少人力成本。那么,微型导轨在自动…

【ESP32】DIY一个电子测光仪

这里写目录标题 0 前言1 开箱2 过程2.1 下载固件2.2 烧录固件2.3 编程环境 Thonny2.4 点灯大师2.5 TFT屏幕2.6 BH1750传感器 成果展示 0 前言 开发板:ESP32-S3-5691 开发环境:circuitpythonthony 1 开箱 2 过程 2.1 下载固件 使用circuitpython的方式开…

MSA+抑郁症模型总结

✨✨ 欢迎大家来访Srlua的博文(づ ̄3 ̄)づ╭❤ 🌟🌟 欢迎各位亲爱的读者,感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢,在这里我会分享我的知识和经验。🎥 希望在…

解决Jenkins使用 Git 参数插件拉取 commit 列表缓慢问题

Jenkins使用 Git 参数插件拉取 commit 列表缓慢问题 项目问题问题描述解决方案具体实现 项目问题 在 Jenkins 中使用 Git 参数插件 进行参数化构建,具有多方面的重要性和好处。这不仅提高了构建的灵活性和透明度,还能大大提升开发和运维效率。以下是使用…

黑马智数Day7

获取行车管理计费规则列表 封装接口 export function getRuleListAPI(params) {return request({url: parking/rule/list,params}) } 获取并渲染数据 import { getRuleListAPI } from /apis/carmounted() {this.getRuleList() }methods: {// 获取规则列表async getRuleList(…

员工电脑怎么监控?这些电脑监控软件必备

在当今远程办公、灵活工时盛行的时代,如何掌握员工的在线活动、确保工作效率和数据安全成为许多企业关注的焦点。电脑监控软件作为管理工具中的关键一环,可以有效帮助企业了解员工的在线行为,避免效率低下和数据泄露等风险。今天我们就来介绍…

学习干货|实战学习应急响应之Windows日志分析,网络安全零基础入门到精通教程!

前言 本次环境将从大赛内与实战环境相结合去了解在应急响应中Windows日志分析的几个关键点,符合大赛及真实环境案例,本次环境将从WEB层面的日志分析到主机内的几种关键日志分析和重点功能进行排查 题目描述:某台Windows服务器遭到攻击者入侵…

零基础光伏人,数据计算轻松拿捏

在可再生能源领域,光伏产业以其清洁、可再生的特点日益受到全球关注。然而,对于初学者或“零基础光伏人”而言,光伏项目涉及的一系列数据计算和专业知识往往显得复杂而难以入手。幸运的是,随着技术的进步,一系列光伏计…

一文搞懂链表相关算法

目录 链表的逆序和截断 逆序 截断 查找链表的中间节点 力扣题 博主主页:东洛的克莱斯韦克-CSDN博客 链表的逆序和截断 逆序 推荐使用头插法逆序,首先要 new 一个虚拟头节点——newNode。如下图 链表的头节点为head,由cur指针指向head&a…

红外热成像技术开启光伏检测新视界

随着全球对可再生能源需求的不断增加,光伏发电系统的应用日益广泛。然而,光伏组件在长期运行中可能会出现各种故障,如热斑效应、隐裂、接线盒故障等,这些问题不仅影响光伏系统的发电效率,还可能引发安全隐患。 红外热成…

基于vue框架的的社区智慧养老系统1mo30(程序+源码+数据库+调试部署+开发环境)

系统程序文件列表 项目功能:老人,员工,老人档案,养生视频,社区医生,就医信息,在线咨询,咨询回复,菜品信息,点餐订单,服务预约,通知信息,服务评价,健康关爱,新闻公告,监控日志 开题报告内容 以下是一份基于Vue框架的社区智慧养老系统的开题报告,详细阐述…

龙蜥8.6 配置用户登录次数和锁定策略(已亲测)

操作系统:龙蜥8.6 x86_64 查看是否安装pam模块 rpm -qa | grep pam 查看可以使用的认证模块,因为有的系统是pam_tally2. cd /etc/pam.d ls 经过查看,该服务器是使用的pam_faillock 模块 打开/etc/pam.d/password-auth 的 PAM 配置文件…

【6.4】位运算-判断是否存在重复元素

一、题目 给定一个整数数组,判断 是否存在重复元素 。如果存在一值在数组中 出现至少两次 ,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。 示例 1: 输入: [ 1 , 2 , 3 , 1 ] 输出: true 示例 2: 输入: [ 1 , 2 , 3 , 4 ] 输出…

PCB打样下单流程

PCB打样下单流程 一、PCB打样在线下单流程1.平台登录2.PCB打样领券3.进入下单系统4.上传PCB文件5.PCB订单界面 PCB(印刷电路板)打样是验证设计、优化性能和推进项目进度的关键环节。随着互联网的…

Python爬虫知识体系-----正则表达式-----持续更新

数据科学、数据分析、人工智能必备知识汇总-----Python爬虫-----持续更新:https://blog.csdn.net/grd_java/article/details/140574349 文章目录 一、正则基础1. 为什么使用正则2. 正则与re模块简介 二、正则表达式1. 匹配单个字符与数字2. 限定符3. 定位符4. 选择匹…