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

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.lryc.cn/news/486057.html

相关文章:

  • 向潜在安全信息和事件管理 SIEM 提供商提出的六个问题
  • 蓝桥杯每日真题 - 第15天
  • Python的Matplotlib
  • Python数据分析:分组转换transform方法
  • 高效灵活的Django URL配置与反向URL实现方案
  • 深入探讨 MySQL 配置与优化:从零到生产环境的最佳实践20241112
  • Java-Redisson分布式锁+自定义注解+AOP的方式来实现后台防止重复请求扩展
  • Java 全栈知识体系
  • 树状数组+概率论,ABC380G - Another Shuffle Window
  • 机器学习day1-数据集
  • 【Golang】——Gin 框架中的路由与请求处理
  • nuxt3添加wowjs动效
  • 我们是如何实现 TiDB Cloud Serverless 的 - 成本篇
  • PCL算法汇总
  • sql注入之二次注入(sqlilabs-less24)
  • Android compose 软键盘 遮挡对话框中TextField 输入框
  • spring-data-elasticsearch 3.2.4 实现桶bucket排序去重,实现指定字段的聚合搜索
  • 【项目开发】分析六种常用软件架构
  • 算法和程序的区别
  • 用指针遍历数组
  • 《Probing the 3D Awareness of Visual Foundation Models》论文解析——多视图一致性
  • 使用pip安装esp32的擦除、写入固件的esptool库
  • 传奇996_23——杀怪掉落,自动捡取,捡取动画
  • 【030】基于51单片机甲醛检测报警器【Proteus仿真+Keil程序+报告+原理图】
  • 微信小程序:vant组件库安装步骤
  • 处理namespace问题:Namespace not specified for AGP 8.0.0
  • C++(Qt)软件调试---内存分析工具Heob(26)
  • Redis五大基本类型——String字符串命令详解(命令用法详解+思维导图详解)
  • Flutter中的Material Theme完全指南:从入门到实战
  • Python 第三方库 PyQt5 的安装