Go八股(Ⅵ)Goroutine 以及其中的锁和思想

Goroutine与并发编程的关系

什么是并发

是指多个任务在同一时间段内进行处理,但不一定是在同一时刻执行。并发强调的是“结构上的并行性”,也就是说,程序能够在一个时间端内同时处理多个任务,但是这些任务可能是交替进行的。例如:一个cpu可以迅速切换任务的上下文,给人一种多个任务在“并行”执行的感觉。

并发的主要特点是能够处理多个任务,而这些任务并不需要同时执行,但在某种情况下它们可以并行执行。并发的关键在于如何有效的切换任务和处理共享资源。

并发的核心就是让多个任务有机会被处理,而不需要等待其他任务完全结束。

goroutine与并发的关系

在Go语言中,goruoutine是并发的基本单位,它是一种轻量级的线程,由Go运行时管理。与传统的线程相比,goroutine占用的资源非常小,因此在程序中启动成千上万个goroutine,不会造成过高的开销。

goroutine通过Go的调度器进行管理,调度器负责讲goroutine映射到操作系统的线程上。goroutine在并发模型中的作用是允许程序处理多个任务,它的本质是轻量级的“线程”,但由于Go提供了对goroutine的高效调度,goroutine的使用非常灵活和高效。

goroutine 如何实现并发?
  • 轻量级任务:goroutine 被设计为非常轻量级的任务。启动一个 goroutine 需要的内存比传统线程要少很多,因此你可以并发执行成千上万的 goroutine。
  • 自动调度:Go 运行时内置的调度器会自动将 goroutine 映射到 CPU 上的可用线程上,从而实现并发执行。Go 程序不需要手动管理线程池或调度机制。
  • 非阻塞执行:在执行 goroutine 时,即使某个 goroutine 因为 I/O 操作或其他原因阻塞了,Go 的调度器仍然可以将其他 goroutine 调度到 CPU 上继续执行,从而保持并发运行。

Go语言中的锁

互斥锁,读写锁

锁的思想:乐观锁,悲观锁

互斥锁()

1.1定义

互斥锁是一个用于保护临界区的同步用语,它的所用是确保统一时刻只有一个goroutine能访问被保护的共享资源。通过互斥锁,可以避免并发访问带来的数据竞争问题。

1.2示例代码:

package mainimport ("fmt""sync"
)var (mutex sync.Mutexcount int
)func increment() {mutex.Lock()   // 获取锁count++mutex.Unlock() // 释放锁
}func main() {var wg sync.WaitGroup// 启动多个 goroutine 并发执行for i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait() // 等待所有 goroutine 完成fmt.Println("Final count:", count)
}

在这个示例中,mutex 被用来确保同一时刻只有一个 goroutine 能够访问 count 变量。由于有多个 goroutine 在并发地执行 increment 函数,通过 mutex.Lock()mutex.Unlock() 确保了线程安全。

1.3注意事项

  • 死锁:如果一个 goroutine 在持有锁时没有及时释放锁,或者两个 goroutine 相互等待对方释放锁,可能会导致死锁。为了避免死锁,确保每个 Lock() 都有对应的 Unlock(),并且尽量减少锁持有的时间。
  • 锁的嵌套:避免在持有锁时再次请求锁,除非非常必要,否则会增加死锁的风险。

读写锁

2.1 定义与原理

读写锁(RWMutex)是为了优化读操作频繁的场景设计的锁。与互斥锁不同,读写锁允许多个 goroutine 同时进行读操作,但写操作是互斥的,只有在没有任何读操作或写操作进行时,才能进行写操作。

  • RLock():用于加读锁,多个 goroutine 可以同时持有读锁,只要没有其他 goroutine 持有写锁。
  • RUnlock():释放读锁。
  • Lock():用于加写锁,写锁是独占的,只有一个 goroutine 可以持有写锁。
  • Unlock():释放写锁。

2.2 使用示例

package mainimport ("fmt""sync"
)var (rwMutex sync.RWMutexdata    int
)func read() {rwMutex.RLock()  // 获取读锁defer rwMutex.RUnlock()fmt.Println("Reading data:", data)
}func write(value int) {rwMutex.Lock()   // 获取写锁defer rwMutex.Unlock()data = valuefmt.Println("Writing data:", value)
}func main() {var wg sync.WaitGroup// 启动多个 goroutine 执行读操作for i := 0; i < 5; i++ {wg.Add(1)go func() {defer wg.Done()read()}()}// 启动一个 goroutine 执行写操作wg.Add(1)go func() {defer wg.Done()write(42)}()wg.Wait()
}

在这个示例中,read 函数通过 rwMutex.RLock() 获取读锁,允许多个 goroutine 同时执行读取操作。而 write 函数则通过 rwMutex.Lock() 获取写锁,保证写操作是独占的,不会与读操作同时进行。

2.3 适用场景

  • 读多写少:读写锁特别适合读多写少的场景。当读操作远远多于写操作时,使用读写锁可以显著提高性能,因为多个读操作可以并发执行,而不必等待锁的释放。
  • 写操作必须独占:写操作会阻塞所有读操作和其他写操作,确保写操作的安全性。

2.4 与互斥锁的区别

  • 互斥锁是严格的互斥机制,即同一时刻只能有一个 goroutine 执行临界区代码(无论是读还是写)。
  • 读写锁允许多个 goroutine 同时读共享资源,但在写共享资源时,写操作需要独占资源。

当程序中存在大量读操作时,读写锁可以提供更高的并发性,因为它允许多个读操作同时进行,而不会像互斥锁那样相互阻塞。

3. 何时使用互斥锁,何时使用读写锁?

  • 使用互斥锁
    • 如果操作是读写混合的,或者读写的比例相当,使用互斥锁会更简单直接。
    • 如果共享资源的访问量很小,读写锁可能带来的复杂性不值得使用。
  • 使用读写锁
    • 如果你的应用场景下,读操作远多于写操作,使用读写锁可以提高性能,允许多个 goroutine 并发读数据,同时保证写操作的独占性。
    • 读写锁比互斥锁更复杂,因此如果读写比不明显,使用互斥锁可能更为简单 

两种锁的思想:悲观锁和乐观锁 

悲观锁

2.1 定义与原理

悲观锁的核心思想是假设并发操作会引发冲突,因此在访问共享资源时,采用加锁的方式防止其他 goroutine 同时访问该资源。只有在获取锁之后,才能对共享资源进行操作,其他 goroutine 必须等待锁被释放。

  • 特点:总是先加锁,再访问资源,保证在同一时刻只有一个 goroutine 能够访问资源。
  • 锁的粒度:锁的粒度较大(例如,一次获取整个资源的锁),因此会影响系统的并发性能。

在 Go 中,sync.Mutexsync.RWMutex 就是典型的悲观锁实现:

  • sync.Mutex:互斥锁,整个资源加锁,防止其他 goroutine 访问。
  • sync.RWMutex:读写锁,写操作是互斥的,而读操作可以并发执行,但在写操作时,所有读操作和其他写操作会被阻塞。

2.2 示例

假设我们有一个共享变量,需要通过悲观锁来保护:

package mainimport ("fmt""sync"
)var (mutex sync.Mutexcount int
)func increment() {mutex.Lock()   // 获取锁defer mutex.Unlock()count++
}func main() {var wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait()fmt.Println("Final count:", count)
}

在这个示例中,mutex.Lock()mutex.Unlock() 确保了只有一个 goroutine 能访问 count 变量,这就是一种典型的悲观锁机制。

2.3 使用场景

悲观锁通常用于以下场景:

  • 资源竞争严重:当多个 goroutine 频繁争抢资源时,悲观锁能确保对资源的访问是互斥的,避免并发问题。
  • 数据一致性要求高:当你无法接受并发操作对数据产生不一致时,使用悲观锁可以避免这类问题。

然而,悲观锁可能导致性能瓶颈,因为锁住了共享资源的访问,其他 goroutine 必须等待锁释放,导致并发性降低。

乐观锁

3.1 定义与原理

与悲观锁相对,乐观锁的核心思想是假设并发操作不会引发冲突,因此它不主动加锁,而是乐观地认为不会有并发冲突。在操作共享资源之前,乐观锁并不会加锁,而是在操作后检测是否发生了冲突。如果冲突发生,则回滚操作,重试或者放弃。

乐观锁通常通过一些“版本控制”机制来实现,例如通过“版本号”或者“时间戳”来判断数据是否发生变化。

  • 特点:乐观锁不会阻塞其他 goroutine,只有在数据写入时才检测冲突。通常适用于读多写少的场景。
  • 回滚重试机制:如果在提交操作时发现冲突,乐观锁通常会选择回滚并重试。

3.2 示例

Go 本身并没有直接实现乐观锁,但你可以通过一些简单的机制来模拟乐观锁。一个常见的方法是通过版本号(或时间戳)来判断资源是否被修改。

package mainimport ("fmt""sync"
)type Counter struct {value intversion int // 版本号,用于乐观锁mu sync.Mutex
}func (c *Counter) increment() bool {c.mu.Lock()defer c.mu.Unlock()// 记录当前版本号currentVersion := c.version// 执行增量操作c.value++// 模拟乐观锁:如果版本号没有变化,认为没有并发冲突if c.version == currentVersion {c.version++return true // 操作成功}// 如果版本号发生变化,表示有其他操作修改了数据,返回失败return false
}func main() {var wg sync.WaitGroupcounter := &Counter{}for i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()// 模拟乐观锁重试for !counter.increment() {fmt.Println("Retrying...")}}()}wg.Wait()fmt.Println("Final count:", counter.value)
}

3.3 使用场景

乐观锁适用于以下场景:

  • 读操作远多于写操作:在大多数情况下,数据不会发生冲突,乐观锁能够减少不必要的加锁,提高性能。
  • 冲突发生概率低:如果你知道并发冲突发生的概率非常低,使用乐观锁可以提高效率。
  • 需要高并发读操作:乐观锁不需要加锁,允许多个 goroutine 同时读取数据,适用于高并发读场景。

然而,乐观锁的缺点在于需要处理冲突时的回滚和重试,尤其是在高并发写操作的场景中,重试的开销可能非常大。

乐观锁与悲观锁的对比

特性悲观锁乐观锁
假设假设并发冲突会发生,采取措施避免冲突假设并发冲突不会发生,事后检查冲突
加锁方式总是加锁,互斥访问不加锁,事后检查冲突
性能可能影响并发性能,锁竞争时会阻塞高并发读取时性能较好,但有回滚重试开销
适用场景资源竞争严重,数据一致性要求高读多写少,冲突概率低的场景

总结

  • 悲观锁:通过加锁保证数据一致性,适用于资源竞争严重的场景,但会影响系统的并发性能。
  • 乐观锁:假设没有冲突,允许多个 goroutine 并发访问资源,适用于读多写少、冲突概率低的场景,通常需要进行冲突检测和回滚。

在 Go 语言中,虽然我们大多数情况下使用的是悲观锁(例如 sync.Mutexsync.RWMutex),但在某些场景下,理解并应用乐观锁的思想可以帮助我们提升并发性能,尤其是在读操作远多于写操作的情况下。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue3【实战】切换全屏【组件封装】FullScreen.vue

效果预览 原理解析 使用 vueUse 里的 useFullscreen() 实现 代码实现 技术方案 vue3 vite UnoCSS vueUse 组件封装 src/components/FullScreen.vue <template><component:is"tag"click"toggle":class"[!isFullscreen ? i-ep:full-sc…

GPIO相关的寄存器(重要)

目录 一、GPIO相关寄存器概述 二、整体介绍 三、详细介绍 1、端口配置低寄存器&#xff08;GPIOx_CRL&#xff09;&#xff08;xA...E&#xff09; 2、端口配置高寄存器&#xff08;GPIOx_CRH&#xff09;&#xff08;xA...E&#xff09; 3、端口输入数据寄存器&#xff…

【Hadoop实训】Hive 数据操作②

延续上一篇文章&#xff0c;不懂的宝子们请看以下链接&#xff1a; 【Hadoop实训】Hive 数据操作①-CSDN博客 目录 一、Group by 语句 (1)、计算emp表每个部门的平均工资 (2)、计算emp表每个部门中每个岗位的最高工资 二、Having 语句 (1)、求每个部门的平均工资 (2)、求每个…

Vulhub漏洞复现---solr---CVE-2019-17558

漏洞描述 Apache Solr 5.0.0到Apache Solr 8.3.1容易受到通过VelocityResponseWriter执行的远程代码的攻击。Velocity模板可以通过configset ’ Velocity / 目录中的Velocity模板或作为参数提供。用户定义的configset可以包含可呈现的、潜在的恶意模板。参数提供的模板在默认情…

ADS学习笔记 5. 微带天线设计

基于ADS2023 update2 参考书籍&#xff1a;卢益锋老师《ADS射频电路设计与仿真学习笔记》 更多笔记&#xff1a;ADS学习笔记 1. 功率放大器设计ADS学习笔记 2. 低噪声放大器设计ADS学习笔记 3. 功分器设计ADS学习笔记 4. 微带分支定向耦合器设计 目录 0、设计指标 1、微带…

【JAVA】使用IDEA创建maven聚合项目

【JAVA】使用IDEA创建maven聚合项目 1.效果图 2.创建父模块项目 2.1删除父模块下面的src目录以及不需要的maven依赖 3创建子模块项目 3.1右击父模块项目选择Module… 3.2创建子模块 3.3删除子模块下不需要的maven依赖 4.子模块创建完成后引入SpringBoot依赖启动项目

一文详细深入总结服务器选型

1. 题记&#xff1a; 服务器选型工作是项目规划检讨的一项非常重要的工作&#xff0c;本文详细深入总结服务器选型。 2. 服务器基础知识概览 2.1 服务器的定义与功能 2.1 .1 定义 服务器是一种高性能计算机&#xff0c;其设计目的是在网络中提供服务。它可以处理来自多个客…

如何编译 Cesium 源码

如何编译 Cesium 源码 Cesium 是一个开源的 JavaScript 库&#xff0c;用于构建 3D 地球和地图应用程序。它提供了一套强大的 API 和工具&#xff0c;使开发者能够创建丰富的地理空间应用。本文将指导您如何从 GitHub 下载 Cesium 源码&#xff0c;并在本地进行编译。 TilesB…

车载诊断架构 --- 关于DTC的开始检测条件

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所有人的看法和评价都是暂时的,只有自己的经历是伴随一生的,几乎所有的担忧和畏惧,都是来源于自己的想象,只有你真的去做了,才会发现有多快乐。…

AI大模型(一):Prompt AI编程

一、Prompt Engineering&#xff0c;提示工程 提示工程也叫指令工程&#xff1a; Prompt是发给大模型的指令&#xff0c;比如【讲个睡前故事】、【用Python写个消消乐游戏】等&#xff1b;本质上大模型相关的工程工作&#xff0c;都是围绕prompt展开的&#xff1b;提示工程门…

如何在 Ubuntu 上安装 Jellyfin 媒体服务器

Jellyfin 是一个开源的媒体服务器软件&#xff0c;让你可以整理、管理和流式传输你的个人媒体收藏&#xff0c;比如电影、音乐、电视节目和照片&#xff0c;而且完全免费&#xff0c;没有订阅费用或数据收集的担忧。 简介 媒体管理&#xff1a;Jellyfin 整理媒体库&#xff0…

数据结构(初阶4)---循环队列详解

循环队列 1.循环队列的结构  1).逻辑模式 2.实现接口  1).初始化  2).判断空和满  3).增加  4).删除  5).找头  6).找尾 3.循环队列的特点 1.循环队列的结构 1).逻辑模式 与队列是大同小异的&#xff0c; 其中还是有一个指向队列头的head指针&#xff0c; 也有一个指向尾…

Qwen2.5-Coder-32B-Instruct Docker 部署openai接口

Qwen2.5-Coder-32B-Instruct 模型下载,国内快捷方式: conda create -n modelscope python=3.10 conda activate modelscopepip install modelscopemodelscope download --model Qwen/Qwen2.5-Coder-32B-Instruct --local_dir /ssd/xiedong/Qwen/Qwen2.5-Coder-32B-I

图形几何之美系列:二维凸包艺术赏析

“凸包是计算几何中的概念&#xff0c;凸包在多个领域中有广泛的应用&#xff0c;主要包括几何计算、图形处理、优化问题、路径规划等。” 1.前言 凸包话题包括二维凸包、三维凸包以及高维凸包。对于平面点集&#xff0c;探究如何构造可以覆盖给定点集最小的凸多边形&#xff1…

速通前端篇 —— HTML

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a;速通前端 目录 HTML的介绍 如何创建HTML文件 HTML 文件基本结构 HTML常用标签 title标签 标题标签 h1-h6 段落标签 p 换行标签 b…

shell编程--永久环境变量和字符串显位

环境变量 echo $HOME 在终端输出后会显示家目录有个root变量 我们会提出个疑问为什么平时我们在终端输入sl 或者which等等命令会输出一些内容呢&#xff0c;这是因为这些命令都有对应的环境变量。 我们查看一下环境变量 在终端输入&#xff1a; echo $PATH 我们看一下输出…

Python毕业设计选题:基于django+vue的二手物品交易系统

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 用户管理 店铺管理 二手物品管理 广告管理 留言反馈 订单…

Spring:bean的配置

对于bean的配置中&#xff0c;主要会讲解bean基础配置,bean的别名配置,bean的作用范围配置(重点),这三部分内容&#xff1a; bean基础配置 id与class配置 bean的name属性 bean的别名配置 bean作用范围scope配置 scope使用后续思考 介绍完scope属性以后&#xff0c;我们…

前端无感刷新token

摘要&#xff1a; Axios 无感知刷新令牌是一种在前端应用中实现自动刷新访问令牌&#xff08;access token&#xff09;的技术&#xff0c;确保用户在进行 API 请求时不会因为令牌过期而中断操作 目录概览 XMLHttpRequestAxiosFetch APIJQuni.request注意事项&#xff1a; 访问…

图形 2.6 伽马校正

伽马校正 B站视频&#xff1a;图形 2.6 伽马校正 文章目录 伽马校正颜色空间传递函数 Gamma校正校正过程为什么需要校正&#xff1f;CRT与转换函数 为什么sRGB在Gamma 0.45空间&#xff1f; 人对亮度的敏感韦伯定律中灰值 线性工作流不在线性空间下进行渲染的问题统一到线性空…