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

【Golang 面试 - 进阶题】每日 3 题(二)

✍个人博客:Pandaconda-CSDN博客

📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力

4. R WMutex 实现

sync.RWMutex 是 Go 语言标准库提供的一种读写锁,用于在多个 goroutine 同时访问共享资源时进行保护。与 sync.Mutex 类似,sync.RWMutex 也是通过互斥锁实现的,但是它允许多个 goroutine 同时获取读锁,而只允许一个 goroutine 获取写锁。

下面是一个简单的 sync.RWMutex 的实现示例:

type RWMutex struct {writerSem    chan struct{} // 写者用的信号量readerSem    chan struct{} // 读者用的信号量readerCount  int           // 当前持有读锁的goroutine数量writerCount  int           // 当前持有写锁的goroutine数量readerWait   int           // 正在等待读锁的goroutine数量writerWait   int           // 正在等待写锁的goroutine数量writerLocked bool          // 是否有goroutine持有写锁
}
func NewRWMutex() *RWMutex {return &RWMutex{writerSem: make(chan struct{}, 1),readerSem: make(chan struct{}, 1),}
}
// 获取读锁
func (m *RWMutex) RLock() {// 获取读锁的过程需要加锁m.writerSem <- struct{}{} // 防止写者获取锁m.readerSem <- struct{}{} // 获取读锁的信号量// 更新状态m.readerCount++if m.writerLocked || m.writerWait > 0 {m.readerWait++<-m.readerSem // 等待写者释放锁m.readerWait--}// 释放加锁时获取的信号量<-m.writerSem
}
// 释放读锁
func (m *RWMutex) RUnlock() {// 获取读锁的过程需要加锁m.writerSem <- struct{}{} // 防止写者获取锁// 更新状态m.readerCount--if m.readerCount == 0 && m.writerWait > 0 {<-m.writerSem // 优先唤醒写者}// 释放加锁时获取的信号量<-m.writerSem
}
// 获取写锁
func (m *RWMutex) Lock() {// 获取写锁的过程需要加锁m.writerSem <- struct{}{} // 防止其他写者获取锁m.writerWait++// 等待其他goroutine释放读锁或写锁for m.writerLocked || m.readerCount > 0 {<-m.readerSem}// 更新状态m.writerWait--m.writerLocked = true// 释放加锁时获取的信号量<-m.writerSem
}
// 释放写锁
func (m *RWMutex) Unlock() {// 获取写锁的过程需要加锁m.writerSem <- struct{}{}// 更新状态m.writerLocked = falseif m.writerWait > 0 {<-m.writerSem} else if m.readerWait > 0 {for i := 0; i < m.readerCount; i++ {m.readerSem <- struct{}{} // 优先唤醒读者}}// 释放加锁时获取的信号量<-m.writerSem
}

在这个实现中,sync.RWMutex 包含以下成员:

  • writerSemreaderSem:两个用于同步的信号量通道。写锁会在 writerSem 上等待,读锁会在 readerSem 上等待。

  • readerCountwriterCount:当前持有读锁和写锁的 goroutine 数量。

  • readerWaitwriterWait:正在等待读锁和写锁的 goroutine 数量。

  • writerLocked:标记当前是否有 goroutine 持有写锁。

在读锁和写锁获取和释放的过程中,都需要先获取 writerSem 信号量防止其他写者获取锁。获取读锁时还需要获取 readerSem 信号量,而获取写锁时需要等待其他 goroutine 释放读锁或写锁。

这个实现中有两个重要的细节:

  • 优先唤醒写者:在释放读锁或写锁时,如果有正在等待的写锁 goroutine,应该优先唤醒它们,因为写锁的优先级更高。

  • 读锁的等待问题:在等待读锁的 goroutine 中,如果有其他 goroutine 正在持有写锁或等待写锁,那么这些读锁 goroutine 应该等待写锁 goroutine 释放锁,避免因等待读锁而导致写锁饥饿。

5. R WMutex 注意事项

  • RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁。

  • 读锁占用的情况下会阻止写,不会阻止读,多个 Goroutine 可以同时获取读锁。

  • 写锁会阻止其他 Goroutine(无论读和写)进来,整个锁由该 Goroutine 独占。

  • 适用于读多写少的场景。

  • RWMutex 类型变量的零值是一个未锁定状态的互斥锁。

  • RWMutex 在首次被使用之后就不能再被拷贝。

  • RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic。

  • RWMutex 的一个写锁去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁。

  • RWMutex 的读锁不要用于递归调用,比较容易产生死锁。

  • RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。

  • 写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁。

  • 读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的 Goroutine,其中等待时间最长的一个 Goroutine 会被唤醒。

 6. Go 读写锁的实现原理?

概念:

读写互斥锁 RWMutex,是对 Mutex 的一个扩展,当一个 goroutine 获得了读锁后,其他 goroutine 可以获取读锁,但不能获取写锁;当一个 goroutine 获得了写锁后,其他 goroutine 既不能获取读锁也不能获取写锁(只能存在一个写者或多个读者,可以同时读)。

使用场景:

多于的情况(既保证线程安全,又保证性能不太差)。

底层实现结构:

互斥锁对应的是底层结构是 sync.RWMutex 结构体,,位于 src/sync/rwmutex.go 中

type RWMutex struct {w           Mutex  // 复用互斥锁writerSem   uint32 // 信号量,用于写等待读readerSem   uint32 // 信号量,用于读等待写readerCount int32  // 当前执行读的 goroutine 数量readerWait  int32  // 被阻塞的准备读的 goroutine 的数量
}

操作:

读锁的加锁与释放

func (rw *RWMutex) RLock() // 加读锁
func (rw *RWMutex) RUnlock() // 释放读锁

加读锁

func (rw *RWMutex) RLock() {
// 为什么readerCount会小于0呢?往下看发现writer的Lock()会对readerCount做减法操作(原子操作)if atomic.AddInt32(&rw.readerCount, 1) < 0 {// A writer is pending, wait for it.runtime_Semacquire(&rw.readerSem)}
}

atomic.AddInt32(&rw.readerCount, 1) 调用这个原子方法,对当前在读的数量加 1,如果返回负数,那么说明当前有其他写锁,这时候就调用 runtime_SemacquireMutex 休眠当前 goroutine 等待被唤醒。

释放读锁

解锁的时候对正在读的操作减 1,如果返回值小于 0 那么说明当前有在写的操作,这个时候调用 rUnlockSlow 进入慢速通道。

func (rw *RWMutex) RUnlock() {if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {rw.rUnlockSlow(r)}
}

被阻塞的准备读的 goroutine 的数量减 1,readerWait 为 0,就表示当前没有正在准备读的 goroutine 这时候调用 runtime_Semrelease 唤醒写操作。

func (rw *RWMutex) rUnlockSlow(r int32) {// A writer is pending.if atomic.AddInt32(&rw.readerWait, -1) == 0 {// The last reader unblocks the writer.runtime_Semrelease(&rw.writerSem, false, 1)}
}

写锁的加锁与释放。

func (rw *RWMutex) Lock() // 加写锁
func (rw *RWMutex) Unlock() // 释放写锁

加写锁

const rwmutexMaxReaders = 1 << 30
func (rw *RWMutex) Lock() {// First, resolve competition with other writers.rw.w.Lock()// Announce to readers there is a pending writer.r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders// Wait for active readers.if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {runtime_Semacquire(&rw.writerSem)}
}

首先调用互斥锁的 lock,获取到互斥锁之后,如果计算之后当前仍然有其他 goroutine 持有读锁,那么就调用 runtime_SemacquireMutex 休眠当前的 goroutine 等待所有的读操作完成

这里readerCount 原子性加上一个很大的负数,是防止后面的协程能拿到读锁,阻塞读

释放写锁

func (rw *RWMutex) Unlock() {// Announce to readers there is no active writer.r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)// Unblock blocked readers, if any.for i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false)}// Allow other writers to proceed.rw.w.Unlock()
}

解锁的操作,会先调用 atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders) 将恢复之前写入的负数,然后根据当前有多少个读操作在等待,循环唤醒

注意点:

  • 读锁或写锁在 Lock() 之前使用 Unlock() 会导致 panic 异常。

  • 使用 Lock() 加锁后,再次 Lock() 会导致死锁(不支持重入),需 Unlock() 解锁后才能再加锁。

  • 锁定状态与 goroutine 没有关联,一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。

互斥锁和读写锁的区别:

  • 读写锁区分读者和写者,而互斥锁不区分。

  • 互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 一篇文章解决Webpack
  • 【数据结构】了解哈希表,解决哈希冲突,用Java模拟实现哈希桶
  • 数据结构与算法 - 递归
  • 大龄程序员转型攻略:拥抱人工智能,开启新征程
  • 截止频率为声波传播的频率上、下限?如何区分两者的关系
  • Golang | Leetcode Golang题解之第301题删除无效的括号
  • Can we Deploy Web Application in Azure OpenAI of Production Level
  • ElasticSearch 关于搜索,有哪些类型的搜索
  • 【C++从小白到大牛】类和对象
  • 【Boot】华硕B85 Pro Gamer 刷NVME驱动【2024年8月1日】
  • 一文看懂Java反射、注解、UML图和Lambda表达式
  • 创邻科技Galaxybase银河图数据库赋能供应链高效协同
  • [Git场景]常用工作场景演练
  • 数据库事务处理技术——故障恢复
  • mysql 数据导入与导出
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • ECS应用管理最佳实践
  • MobX
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • Spark学习笔记之相关记录
  • spring + angular 实现导出excel
  • 电商搜索引擎的架构设计和性能优化
  • 规范化安全开发 KOA 手脚架
  • 好的网址,关于.net 4.0 ,vs 2010
  • 免费小说阅读小程序
  • 目录与文件属性:编写ls
  • 少走弯路,给Java 1~5 年程序员的建议
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 使用 @font-face
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 微信小程序--------语音识别(前端自己也能玩)
  • 项目实战-Api的解决方案
  • Android开发者必备:推荐一款助力开发的开源APP
  • mysql面试题分组并合并列
  • ​Linux Ubuntu环境下使用docker构建spark运行环境(超级详细)
  • #includecmath
  • #NOIP 2014# day.1 生活大爆炸版 石头剪刀布
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (C语言)球球大作战
  • (LLM) 很笨
  • (PySpark)RDD实验实战——取最大数出现的次数
  • (附源码)计算机毕业设计SSM疫情社区管理系统
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (十三)Flink SQL
  • (五)activiti-modeler 编辑器初步优化
  • (转)h264中avc和flv数据的解析
  • (状压dp)uva 10817 Headmaster's Headache
  • *(长期更新)软考网络工程师学习笔记——Section 22 无线局域网
  • *2 echo、printf、mkdir命令的应用
  • .bashrc在哪里,alias妙用
  • .NET 8 跨平台高性能边缘采集网关
  • .NET Core日志内容详解,详解不同日志级别的区别和有关日志记录的实用工具和第三方库详解与示例
  • .net 简单实现MD5
  • .NET 线程 Thread 进程 Process、线程池 pool、Invoke、begininvoke、异步回调
  • .NET轻量级ORM组件Dapper葵花宝典