深入浅出 Go 语言 sync包中的互斥锁、条件变量

深入浅出 Go 语言 sync包中的互斥锁、条件变量

引言

在并发编程中,多个 Goroutine 同时访问共享资源可能会导致数据竞争(Race Condition),进而引发程序的不一致性或崩溃。为了确保并发程序的正确性和稳定性,Go 语言提供了丰富的同步机制,帮助开发者安全地管理共享资源的访问。sync 包是 Go 语言中最常用的同步工具包,它包含了多种同步原语,如互斥锁(Mutex)、读写锁(RWMutex)、条件变量(Cond)等。

本文将深入浅出地介绍 sync 包中的主要同步原语,帮助你理解它们的工作原理和使用方法,并通过实际案例展示如何在并发程序中正确使用这些同步机制。无论你是初学者还是有经验的开发者,本文都将为你提供有价值的参考。


1. 什么是同步原语?

1.1 同步原语的概念

同步原语(Synchronization Primitives)是操作系统和编程语言中用于协调多个线程或 Goroutine 之间访问共享资源的机制。通过同步原语,你可以确保在同一时间只有一个 Goroutine 能够访问某个共享资源,从而避免数据竞争和不一致的问题。

在 Go 语言中,sync 包提供了多种同步原语,常见的包括:

  • 互斥锁(Mutex):用于保护共享资源,确保同一时间只有一个 Goroutine 可以访问该资源。
  • 读写锁(RWMutex):允许多个 Goroutine 同时读取共享资源,但在写操作时只允许一个 Goroutine 访问。
  • 条件变量(Cond):用于在特定条件下唤醒等待的 Goroutine,常用于生产者-消费者模式。
  • WaitGroup:用于等待一组 Goroutine 完成任务。
  • Once:确保某个操作只执行一次。

1.2 为什么需要同步?

在并发编程中,多个 Goroutine 可能会同时访问同一个共享资源,例如全局变量、文件、数据库连接等。如果这些 Goroutine 没有进行适当的同步,可能会导致以下问题:

  • 数据竞争(Race Condition):两个或多个 Goroutine 同时读写同一个变量,导致数据不一致。
  • 死锁(Deadlock):多个 Goroutine 互相等待对方释放资源,导致程序无法继续执行。
  • 竞态条件(Race Condition):程序的行为依赖于 Goroutine 的执行顺序,导致不可预测的结果。

为了避免这些问题,我们需要使用同步原语来协调 Goroutine 之间的访问,确保共享资源的安全性和一致性。


2. 互斥锁(Mutex)

2.1 什么是互斥锁?

互斥锁(Mutex,Mutual Exclusion Lock)是 sync 包中最基本的同步原语之一。它用于保护共享资源,确保同一时间只有一个 Goroutine 可以访问该资源。当一个 Goroutine 获取了互斥锁后,其他 Goroutine 必须等待,直到该 Goroutine 释放锁。

2.1.1 使用互斥锁

在 Go 语言中,sync.Mutex 提供了两个主要方法:

  • Lock():获取互斥锁,如果锁已被占用,则阻塞当前 Goroutine,直到锁被释放。
  • Unlock():释放互斥锁,允许其他 Goroutine 获取锁。

以下是一个简单的例子,展示了如何使用互斥锁保护共享资源:

package mainimport ("fmt""sync"
)// 定义一个结构体,包含共享资源和互斥锁
type Counter struct {mu     sync.Mutex // 互斥锁count  int       // 共享资源
}// 增加计数器的值
func (c *Counter) Increment() {c.mu.Lock()   // 获取锁defer c.mu.Unlock() // 确保在函数结束时释放锁c.count++
}// 获取计数器的值
func (c *Counter) Value() int {c.mu.Lock()   // 获取锁defer c.mu.Unlock() // 确保在函数结束时释放锁return c.count
}func main() {var wg sync.WaitGroupcounter := &Counter{}// 启动 1000 个 Goroutine 来增加计数器for i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()counter.Increment()}()}// 等待所有 Goroutine 完成wg.Wait()// 打印最终的计数器值fmt.Println("最终计数:", counter.Value())
}

在这个例子中,我们定义了一个 Counter 结构体,包含一个共享资源 count 和一个互斥锁 mu。通过 Increment()Value() 方法,我们可以安全地增加和获取计数器的值。Lock()Unlock() 方法用于确保同一时间只有一个 Goroutine 可以访问 count,从而避免数据竞争。

2.2 互斥锁的最佳实践

  • 尽量减少锁的持有时间:长时间持有锁会影响程序的性能,因此应尽量减少锁的持有时间。可以通过将锁的范围限制在最小的代码块内来实现这一点。
  • 避免嵌套锁:如果多个 Goroutine 需要获取多个锁,可能会导致死锁。因此,应尽量避免嵌套锁,或者确保锁的获取顺序一致。
  • 使用 defer 释放锁:在获取锁后,务必确保在函数结束时释放锁。可以使用 defer 关键字来确保即使发生错误或异常,锁也会被正确释放。

3. 读写锁(RWMutex)

3.1 什么是读写锁?

读写锁(RWMutex,Read-Write Mutex)是 sync 包中的一种更灵活的锁机制。与互斥锁不同,读写锁允许多个 Goroutine 同时读取共享资源,但在写操作时只允许一个 Goroutine 访问。这使得读写锁在读多写少的场景下具有更好的性能。

3.1.1 使用读写锁

sync.RWMutex 提供了三个主要方法:

  • RLock():获取读锁,允许多个 Goroutine 同时读取共享资源。
  • RUnlock():释放读锁。
  • Lock():获取写锁,确保同一时间只有一个 Goroutine 可以写入共享资源。
  • Unlock():释放写锁。

以下是一个简单的例子,展示了如何使用读写锁保护共享资源:

package mainimport ("fmt""sync""time"
)// 定义一个结构体,包含共享资源和读写锁
type Cache struct {mu     sync.RWMutex // 读写锁data   map[string]string
}// 设置缓存数据
func (c *Cache) Set(key, value string) {c.mu.Lock()   // 获取写锁defer c.mu.Unlock() // 确保在函数结束时释放锁c.data[key] = value
}// 获取缓存数据
func (c *Cache) Get(key string) string {c.mu.RLock()  // 获取读锁defer c.mu.RUnlock() // 确保在函数结束时释放锁if value, ok := c.data[key]; ok {return value}return ""
}func main() {cache := &Cache{data: make(map[string]string)}// 启动多个 Goroutine 来读取和写入缓存var wg sync.WaitGroup// 写入数据wg.Add(1)go func() {defer wg.Done()cache.Set("key1", "value1")time.Sleep(time.Second) // 模拟写操作的时间}()// 读取数据for i := 0; i < 5; i++ {wg.Add(1)go func(i int) {defer wg.Done()value := cache.Get("key1")fmt.Printf("Goroutine %d 获取到的值: %s\n", i, value)}(i)}// 等待所有 Goroutine 完成wg.Wait()
}

在这个例子中,我们定义了一个 Cache 结构体,包含一个共享资源 data 和一个读写锁 mu。通过 Set()Get() 方法,我们可以安全地写入和读取缓存数据。RLock()RUnlock() 方法用于获取和释放读锁,允许多个 Goroutine 同时读取缓存;Lock()Unlock() 方法用于获取和释放写锁,确保同一时间只有一个 Goroutine 可以写入缓存。

3.2 读写锁的最佳实践

  • 读多写少的场景使用读写锁:如果读操作远远多于写操作,使用读写锁可以显著提高性能,因为多个 Goroutine 可以同时读取共享资源。
  • 避免长时间持有写锁:长时间持有写锁会影响其他 Goroutine 的读取操作,因此应尽量减少写锁的持有时间。
  • 确保读锁和写锁的正确配合:在读写锁的使用中,必须确保读锁和写锁的正确配合,避免出现死锁或数据竞争。

4. 条件变量(Cond)

4.1 什么是条件变量?

条件变量(Cond,Condition Variable)是 sync 包中用于在特定条件下唤醒等待的 Goroutine 的机制。条件变量通常与互斥锁一起使用,允许 Goroutine 在满足某些条件时继续执行,而在条件不满足时进入等待状态。

4.1.1 使用条件变量

sync.Cond 提供了以下方法:

  • NewCond(lock):创建一个新的条件变量,参数 lock 是一个互斥锁,用于保护共享资源。
  • Wait():使当前 Goroutine 进入等待状态,直到其他 Goroutine 调用 Signal()Broadcast() 唤醒它。
  • Signal():唤醒一个正在等待的 Goroutine。
  • Broadcast():唤醒所有正在等待的 Goroutine。

以下是一个简单的例子,展示了如何使用条件变量实现生产者-消费者模式:

package mainimport ("fmt""sync"
)// 定义一个缓冲区,用于存储生产者生成的数据
type Buffer struct {mu      sync.Mutexcond    *sync.Condbuffer  []intcapacity int
}// 初始化缓冲区
func NewBuffer(capacity int) *Buffer {b := &Buffer{buffer:  make([]int, 0, capacity),capacity: capacity,}b.cond = sync.NewCond(&b.mu)return b
}// 生产者向缓冲区添加数据
func (b *Buffer) Produce(data int) {b.mu.Lock()for len(b.buffer) == b.capacity {b.cond.Wait() // 缓冲区已满,等待消费者消费}b.buffer = append(b.buffer, data)fmt.Printf("生产者添加数据: %d\n", data)b.cond.Signal() // 唤醒一个等待的消费者b.mu.Unlock()
}// 消费者从缓冲区获取数据
func (b *Buffer) Consume() int {b.mu.Lock()for len(b.buffer) == 0 {b.cond.Wait() // 缓冲区为空,等待生产者生产}data := b.buffer[0]b.buffer = b.buffer[1:]fmt.Printf("消费者获取数据: %d\n", data)b.cond.Signal() // 唤醒一个等待的生产者b.mu.Unlock()return data
}func main() {buffer := NewBuffer(3)// 启动生产者 Goroutinego func() {for i := 0; i < 5; i++ {buffer.Produce(i)}}()// 启动消费者 Goroutinego func() {for i := 0; i < 5; i++ {buffer.Consume()}}()// 等待一段时间,确保所有 Goroutine 完成time.Sleep(time.Second * 2)
}

在这个例子中,我们定义了一个 Buffer 结构体,用于模拟生产者-消费者模式中的缓冲区。生产者通过 Produce() 方法向缓冲区添加数据,消费者通过 Consume() 方法从缓冲区获取数据。sync.Cond 用于在缓冲区满或空时让生产者或消费者进入等待状态,直到条件满足时被唤醒。

4.2 条件变量的最佳实践

  • 条件变量必须与互斥锁一起使用:条件变量依赖于互斥锁来保护共享资源,因此必须确保在调用 Wait()Signal()Broadcast() 时已经获取了相应的锁。
  • 避免频繁唤醒Signal()Broadcast() 会唤醒等待的 Goroutine,但唤醒过多的 Goroutine 可能会导致性能下降。因此,应尽量减少不必要的唤醒操作。
  • 使用 for 循环检查条件:在调用 Wait() 之前,建议使用 for 循环检查条件是否满足,以避免虚假唤醒(Spurious Wakeup)问题。

5. WaitGroup

5.1 什么是 WaitGroup?

WaitGroupsync 包中用于等待一组 Goroutine 完成任务的同步机制。WaitGroup 通过计数器来跟踪 Goroutine 的完成情况,当计数器为零时,表示所有 Goroutine 已经完成。

5.1.1 使用 WaitGroup

sync.WaitGroup 提供了以下方法:

  • Add(delta):增加或减少计数器的值,通常在启动 Goroutine 之前调用。
  • Done():减少计数器的值,通常在 Goroutine 完成任务时调用。
  • Wait():阻塞当前 Goroutine,直到计数器为零。

以下是一个简单的例子,展示了如何使用 WaitGroup 等待多个 Goroutine 完成任务:

package mainimport ("fmt""sync""time"
)func worker(id int, wg *sync.WaitGroup) {defer wg.Done() // 任务完成后减少计数器fmt.Printf("Worker %d 开始工作的\n", id)time.Sleep(time.Second) // 模拟工作时间fmt.Printf("Worker %d 完成工作的\n", id)
}func main() {var wg sync.WaitGroup// 启动 5 个 Goroutinefor i := 1; i <= 5; i++ {wg.Add(1) // 增加计数器go worker(i, &wg)}// 等待所有 Goroutine 完成wg.Wait()fmt.Println("所有任务已完成")
}

在这个例子中,我们使用 WaitGroup 来等待 5 个 Goroutine 完成任务。每个 Goroutine 在开始时调用 wg.Add(1) 增加计数器,在完成任务时调用 wg.Done() 减少计数器。主 Goroutine 通过 wg.Wait() 阻塞,直到所有子 Goroutine 完成任务。

5.2 WaitGroup 的最佳实践

  • 确保 Add()Done() 成对使用Add()Done() 必须成对使用,否则可能会导致计数器不匹配,导致程序无法正常结束。
  • 避免在 Goroutine 外部调用 Done()Done() 应该在 Goroutine 内部调用,确保只有在 Goroutine 完成任务后才会减少计数器。
  • 使用 defer 调用 Done():在 Goroutine 中使用 defer 调用 Done(),确保即使发生错误或异常,计数器也会被正确减少。

6. Once

6.1 什么是 Once?

Oncesync 包中用于确保某个操作只执行一次的同步机制。sync.Once 通过内部的状态标志来确保即使多个 Goroutine 同时调用 Do() 方法,也只会有一个 Goroutine 执行指定的操作。

6.1.1 使用 Once

sync.Once 提供了以下方法:

  • Do(f func()):执行一次指定的操作 f,如果该操作已经执行过,则不会再次执行。

以下是一个简单的例子,展示了如何使用 Once 确保某个操作只执行一次:

package mainimport ("fmt""sync"
)var once sync.Once
var value intfunc initialize() {fmt.Println("初始化操作...")value = 42
}func main() {// 启动多个 Goroutine 来调用 initialize()var wg sync.WaitGroupfor i := 0; i < 5; i++ {wg.Add(1)go func() {defer wg.Done()once.Do(initialize)fmt.Printf("值: %d\n", value)}()}// 等待所有 Goroutine 完成wg.Wait()
}

在这个例子中,我们使用 sync.Once 来确保 initialize() 只会被调用一次,即使多个 Goroutine 同时调用 once.Do(initialize)sync.Once 通过内部的状态标志来保证这一点,确保即使多个 Goroutine 同时尝试执行 initialize(),也只会有一个 Goroutine 实际执行该操作。

6.2 Once 的最佳实践

  • 确保操作的幂等性sync.Once 保证操作只执行一次,但不能保证操作本身是幂等的。因此,应确保 Do() 中的操作是幂等的,即多次执行不会产生不同的结果。
  • 避免在 Do() 中使用复杂的逻辑Do() 中的操作应该是简单且快速的,避免在其中执行耗时的操作,以免影响程序的性能。

7. 总结

通过本文的学习,你已经掌握了 sync 包中常用的同步原语,包括互斥锁、读写锁、条件变量、WaitGroup 和 Once。这些同步机制能够帮助你在并发编程中安全地管理共享资源,避免数据竞争和不一致的问题。无论是保护共享变量、实现生产者-消费者模式,还是确保某个操作只执行一次,sync 包都为你提供了强大的支持。


参考资料

参考资料

  1. Go 官方文档 - sync 包
  2. Go 语言中文网 - 并发编程与 sync 包

业精于勤,荒于嬉;行成于思,毁于随。

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

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

相关文章

制造业数据集成案例分享:3小时内实现MySQL到MySQL数据对接

ZZ刷新生产用料清单四化库存-制造一处-3小时&#xff1a;MySQL到MySQL数据集成案例分享 在现代制造业中&#xff0c;实时、准确的数据流动是确保生产效率和资源优化的关键。本文将分享一个实际运行的系统对接集成案例——“ZZ刷新生产用料清单四化库存-制造一处-3小时”&#…

OpenCV 图像基本操作

OpenCV快速通关 第一章&#xff1a;OpenCV 图像基本操作 第二章&#xff1a;OpenCV 图像基本操作 OpenCV 图像基本操作 OpenCV快速通关第二章&#xff1a;OpenCV 图像基本操作一、相关结构体与函数介绍&#xff08;一&#xff09;cv::Mat 结构体&#xff08;二&#xff09;cv:…

雨晨 2610(2)0.2510 Windows 11 24H2 Iot 企业版 LTSC 2024 极简 2in1

文件: 雨晨 2610(2)0.2510 Windows 11 24H2 Iot 企业版 LTSC 2024 极简 2in1 install.esd 索引: 1 名称: Windows 11 IoT 企业版 LTSC 极简 26100.2510 描述: Windows 11 IoT 企业版 LTSC 极简 26100.2510 By YCDISM RTM 2025 24-12-07 大小: 8,176,452,990 个字节 索引: 2 …

PHP保存base64编码图片,图片有一部分是灰色块儿,原因和解决办法

文章目录 场景原因解决方案完整的代码前端代码php代码 场景 我有个需求&#xff0c;移动端h5上传多张的图片。用input file可以上传多张&#xff0c;但是现在照片体积越来越大&#xff0c;同时上传多张会因为体积过大&#xff0c;导致上传失败。如果是小程序会好很多&#xff…

【CSP CCF记录】202212-2第28次认证 训练计划

题目 样例1输入 10 5 0 0 0 0 0 1 2 3 2 10 样例1输出 1 1 1 1 1 10 9 8 9 1 样例1解释 五项科目间没有依赖关系&#xff0c;都可以从第 1 天就开始训练。 10天时间恰好可以完成所有科目的训练。其中科目 1 耗时仅 1天&#xff0c;所以最晚可以拖延到第 10 天再开始训练&…

gitee

Git 是一个开源的 [ 分布式 ][ 版本控制系统 ] &#xff0c;用于敏捷高效地 处理任何或小或大的项目 Git 非常容易学习&#xff0c;低植入&#xff0c;高性能。因为拥有轻量的本地分支&#xff0c;易用的暂存区&#xff0c;和多工作流的特点&#xff0c;它超越了类似Subversio…

Spring——SpringBean初始接口

摘要 本文详细介绍了Spring框架中SpringBean的初始化接口和注解&#xff0c;包括BeanPostProcessor接口、InitializingBean接口和PostConstruct注解。文章解释了这些接口和注解的原理、作用、适用场景&#xff0c;并提供了示例代码。最后&#xff0c;对比了不同SpringBean初始…

「嵌入式系统设计与实现」书评:学习一个STM32的案例

本文最早发表于电子发烧友论坛&#xff1a;【新提醒】【「嵌入式系统设计与实现」阅读体验】 学习一个STM32的案例 - 发烧友官方/活动 - 电子技术论坛 - 广受欢迎的专业电子论坛!https://bbs.elecfans.com/jishu_2467617_1_1.html 感谢电子发烧友论坛和电子工业出版社的赠书。 …

操作系统——大容量存储结构

笔记内容及图片整理自XJTUSE “操作系统” 课程ppt&#xff0c;仅供学习交流使用&#xff0c;谢谢。 大容量存储结构概述 磁盘 磁盘为现代计算机系统提供大量外存。每个盘片为平的圆状&#xff08;类似CD&#xff09;&#xff0c;普通盘片直径为4.5~9.0厘米。盘片的两面都涂着…

Redis从入门到进阶(总结)

以下内容均以CentOS7为背景。 一、Redis安装及启动 mysql&#xff08;读&#xff1a;2000/s&#xff1b;写&#xff1a;600/s&#xff09; redis&#xff08;读&#xff1a;10w/s&#xff1b;写&#xff1a;8w/s&#xff09;通过官方给出的数据单机并发可以达到10w/s&#xf…

Java进阶(注解,设计模式,对象克隆)

Java进阶(注解&#xff0c;设计模式&#xff0c;对象克隆) 一. 注解 1.1 什么是注解 java中注解(Annotation)&#xff0c;又称java标注&#xff0c;是一种特殊的注释 可以添加在包&#xff0c;类&#xff0c;成员变量&#xff0c;方法&#xff0c;参数等内容上 注解会随同…

使用 Gin 框架构建 RESTful 博客 API

使用 Gin 框架构建 RESTful 博客 API 引言 在现代 Web 开发中&#xff0c;RESTful API 是一种非常流行的设计风格&#xff0c;它通过 HTTP 协议与客户端进行通信&#xff0c;提供了灵活且易于扩展的接口。Go 语言以其高效的并发处理能力和简洁的语法&#xff0c;成为了构建高…

Leecode刷题C语言之骑士在棋盘上的概率

执行结果:通过 执行用时和内存消耗如下&#xff1a; 代码如下&#xff1a; static int dirs[8][2] {{-2, -1}, {-2, 1}, {2, -1}, {2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}};double knightProbability(int n, int k, int row, int column){double dp[200][30][30];mem…

21. C++STL 7(8000字详解list及其迭代器的模拟实现)

⭐本篇重点&#xff1a;STL中的list及其迭代器的模拟实现和测试 ⭐本篇代码&#xff1a;c学习 橘子真甜/c-learning-of-yzc - 码云 - 开源中国 (gitee.com) 目录 一. list的节点 二. list的迭代器 2.1 迭代器框架 2.2 迭代器实现 三. list的实现 3.1 list的构造函数 3.…

Docker打包SpringBoot项目

一、项目打成jar包 在进行docker打包之前&#xff0c;先确定一下&#xff0c;项目能够正常的打成JAR包&#xff0c;并且启动之后能够正常的访问。这一步看似是可有可无&#xff0c;但是能避免后期的一些无厘头问题。 二、Dockerfile 项目打包成功之后&#xff0c;需要编写Doc…

零基础学鸿蒙开发--第九篇--网络请求

12. ⽹络请求 鸿蒙系统提供了 http 模块 ⽤于发送 http 请求&#xff0c;另外&#xff0c; OpenHarmony社区基于该模块将前端开发中常⽤的⽹络请 求库 axios 移植到了鸿蒙系统&#xff0c;因此我们也可以在鸿蒙系统中使⽤ axios 发送 http 请求&#xff0c;下⾯重点为⼤家介绍…

133.WEB渗透测试-信息收集-小程序、app(4)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;132.WEB渗透测试-信息收集-小程序、app&#xff08;3&#xff09; 输入命令&#xff1a;…

Pointnet++改进71:添加LFE模块|高效长距离注意力网络

简介:1.该教程提供大量的首发改进的方式,降低上手难度,多种结构改进,助力寻找创新点!2.本篇文章对Pointnet++特征提取模块进行改进,加入LFE模块,提升性能。3.专栏持续更新,紧随最新的研究内容。 目录 1.理论介绍 2.修改步骤 2.1 步骤一 2.2 步骤二 2.3 步骤三 1.理…

Android仿美团左右联动购物列表

Android仿美团左右联动购物列表 左右联动购物列表&#xff0c;不难。 一、思路&#xff1a; 两个RecycleView 二、效果图&#xff1a; 三、关键代码&#xff1a; public class MainActivity extends AppCompatActivity {private RecyclerView rl_left;private RecyclerVie…

Mitel MiCollab 企业协作平台 任意文件读取漏洞复现(CVE-2024-41713)

0x01 产品简介 Mitel MiCollab是加拿大Mitel(敏迪)公司推出的一款企业级协作平台,旨在为企业提供统一、高效、安全的通信与协作解决方案。通过该平台,员工可以在任何时间、任何地点,使用任何设备,实现即时通信、语音通话、视频会议、文件共享等功能,从而提升工作效率和…