Go Sync 包:并发的 6 个关键概念

简介: Go Sync 包:并发的 6 个关键概念

1.sync.Mutex和sync.RWMutex


要知道,mutex(互斥)对于我们 gopher 来说就像一个老伙计。在处理 goroutine 时,确保它们不会同时访问资源是非常重要的,而 mutex 可以帮助我们做到这一点。


sync.Mutex


看看这个简单的例子,我没有使用互斥锁来保护我们的变量 a


var a = 0
func Add() {
  a++
}
func main() {
  for i := 0; i < 500; i++ {
    go Add()
  }
  time.Sleep(5 * time.Second)
  fmt.Println(a)
}


此代码的结果是不可预测的。如果幸运的话,您可能会得到 500,但通常结果会小于 500。现在,让我们使用互斥体增强我们的 Add 函数:


var mtx = sync.Mutex{}
func Add() {
  mtx.Lock()
  defer mtx.Unlock()
  a++
}

现在,代码提供了预期的结果。但是使用 sync.RWMutex 呢?


为什么使用 sync.RWMutex?


想象一下,您正在检查 a 变量,但其他 goroutines 也在调整它。您可能会得到过时的信息。那么,解决这个问题的方法是什么?


让我们退后一步,使用我们的旧方法,将 sync.Mutex 添加到我们的 Get() 函数中:


func Add() {
  mtx.Lock()
  defer mtx.Unlock()
  a++
}
func Get() int {
  mtx.Lock()
  defer mtx.Unlock()
  return a
}

但这里的问题是,如果您的服务或程序调用 Get() 数百万次而只调用 Add() 几次,那么我们实际上是在浪费资源,因为我们大部分时间甚至都没有修改它而将所有内容都锁定了。


这就是 sync.RWMutex 突然出现来拯救我们的一天,这个聪明的小工具旨在帮助我们处理同时读取和写入的情况。


var mtx = sync.RWMutex{}
func Add() {
  mtx.Lock()
  defer mtx.Unlock()
  
  a++
}
func Look() {
  mtx.RLock()
  defer mtx.RUnlock()
  
  fmt.Println(a)
}

那么,RWMutex 有什么了不起的呢?好吧,它允许数百万次并发读取,同时确保一次只能进行一次写入。让我澄清一下它是如何工作的:


  • 写入时,读取被锁定。
  • 读取时,写入被锁定。
  • 多次读取不会相互锁定。


sync.Locker


哦对了,Mutex和RWMutex都实现了sync.Locker接口{},签名是这样的:


// A Locker represents an object that can be locked and unlocked.
type Locker interface {
  Lock()
  Unlock()
}

如果你想创建一个接受 Locker 的函数,你可以将这个函数与你的自定义 locker 或同步互斥锁一起使用:


func Add(mtx sync.Locker) {
  mtx.Lock()
  defer mtx.Unlock()
  
  a++
}


2. sync.WaitGroup


您可能已经注意到我使用了 time.Sleep(5 * time.Second) 来等待所有 goroutine 完成,但老实说,这是一个非常丑陋的解决方案。


这就是 sync.WaitGroup 出现的地方:


func main() {
  wg := sync.WaitGroup{}
  for i := 0; i < 500; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      Add()
    }()
  }
  
  wg.Wait()
  fmt.Println(a)
}

sync.WaitGroup 有 3 个主要方法:Add、Done 和 Wait。


首先是 Add(delta int):此方法将 WaitGroup 计数器增加 delta 的值。你通常会在生成 goroutine 之前调用它,表示有一个额外的任务需要完成。


如果我们将 WaitGroup 放在 go func() {} 中,您认为会发生什么?


go func() {
  wg.Add(1)
  defer wg.Done()
  Add()
}()


我的编译器喊道,“应该在启动 goroutine 之前调用 wg.Add(1) 以避免竞争”,我的运行时出现恐慌,“panic: sync: WaitGroup is reused before previous Wait has returned”。


其他两种方法非常简单:


  • 当一个 goroutine 结束它的任务时, Done 被调用。
  • Wait 会阻塞调用者,直到 WaitGroup 计数器归零,这意味着所有派生的 goroutine 都已完成它们的任务。


3. sync.Once


假设您在一个包中有一个 CreateInstance() 函数,但您需要确保它在使用前已初始化。所以你在不同的地方多次调用它,你的实现看起来像这样:


var i = 0
var _isInitialized = false
func CreateInstance() {
  if _isInitialized {
    return
  }
  
  i = GetISomewhere()
  _isInitialized = true
}

但是如果有多个 goroutine 调用这个方法呢? i = GetISomeWhere 行会运行多次,即使您为了稳定性只希望它执行一次。

您可以使用我们之前讨论过的互斥锁,但同步包提供了一种更方便的方法:sync.Once

var i = 0
var once = &sync.Once{}
func CreateInstance() {
  once.Do(func() {
    i = GetISomewhere()
  })
}


使用 sync.Once,你可以确保一个函数只执行一次,不管它被调用了多少次或者有多少 goroutines 同时调用它。


4. sync.Pool


想象一下,你有一个池,里面有一堆你想反复使用的对象。这可以减轻垃圾收集器的一些压力,尤其是在创建和销毁这些资源的成本很高的情况下。

所以,无论何时你需要一个对象,你都可以从池中取出它。当您使用完它时,您可以将它放回池中以备日后重复使用。


var pool = sync.Pool{
  New: func() interface{} {
    return 0
  },
}
func main() {
  pool.Put(1)
  pool.Put(2)
  pool.Put(3)
  
  a := pool.Get().(int)
  b := pool.Get().(int)
  c := pool.Get().(int)
  
  fmt.Println(a, b, c) // Output: 1, 3, 2 (order may vary)
}


请记住,将对象放入池中的顺序不一定是它们出来的顺序,即使多次运行上述代码时顺序也是随机。

让我分享一些使用 sync.Pool 的技巧:


  • 它非常适合长期存在并且有多个实例需要管理的对象,例如数据库连接(1000 个连接?)、worker goroutine,甚至缓冲区。
  • 在将对象返回池之前始终重置对象的状态。这样,您可以避免任何无意的数据泄漏或奇怪的行为。
  • 不要指望池中已经存在的对象,因为它们可能会意外释放。


5. sync.Map


当您同时使用 map 时,有点像使用 RWMutex。您可以同时进行多次读取,但不能进行多次读写或写入。如果存在冲突,您的服务将崩溃而不是覆盖数据或导致意外行为。


这就是 sync.Map 派上用场的地方,因为它可以帮助我们避免这个问题。让我们仔细看看 sync.Map 给我们提供什么:


  • CompareAndDelete (go 1.20):如果值匹配则删除键的条目;如果不存在值或旧值为 nil,则返回 false。
  • CompareAndSwap(go 1.20):如果新旧值匹配,则交换一个键,只要确保旧值是可比较的。
  • Swap (go 1.20):交换键的值并返回旧值(如果存在)。
  • LoadOrStore:获取当前键值或保存并返回提供的值(如果不存在)
  • Range (f func(key, value any):遍历映射,将函数 f 应用于每个键值对。如果 f 说返回 false,它会停止。
  • Store
  • Delete
  • Load
  • LoadAndDelete

Q: 我们为什么不使用带有 Mutex 的常规 map 呢?


我通常选择带有 RWMutex 的 map,但在某些情况下认识到 sync.Map 的强大功能很重要。那么,它真正发光的地方在哪里呢?


如果您有许多 goroutines 访问 map 中的单独键,则具有单个互斥锁的常规 map 可能会导致争用,因为它仅针对单个写操作锁定整个 map。

另一方面,sync.Map 使用更完善的锁定机制,有助于最大限度地减少此类场景中的争用。

6. sync.Cond


将 sync.Cond 视为支持多个 goroutine 等待和相互交互的条件变量。为了更好地理解,让我们看看如何使用它。


首先,我们需要创建带有 Locker 的 sync.Cond:


var mtx sync.Mutex
var cond = sync.NewCond(&mtx)


goroutine 调用 cond.Wait 并等待来自其他地方的信号以继续执行:


func dummyGoroutine(id int) {
  cond.L.Lock()
  defer cond.L.Unlock()
  fmt.Printf("Goroutine %d is waiting...\n", id)
  cond.Wait()
  fmt.Printf("Goroutine %d received the signal.\n", id)
}


然后,另一个 goroutine(就像主 goroutine)调用 cond.Signal(),让我们等待的 goroutine 继续:


func main() {
  go dummyGoroutine(1)
  
  time.Sleep(1 * time.Second)
  fmt.Println("Sending signal...")
  cond.Signal()
  time.Sleep(1 * time.Second)
}


结果如下所示:


Goroutine 1 is waiting...
Sending signal...
Goroutine 1 received the signal.


如果有多个 goroutines 在等待我们的信号怎么办?这就是我们可以使用广播的时候:


func main() {
  go dummyGoroutine(1)
  go dummyGoroutine(2)
  
  time.Sleep(1 * time.Second)
  cond.Broadcast() // broadcast to all goroutines
  time.Sleep(1 * time.Second)
}


结果如下所示:


Goroutine 1 is waiting...
Goroutine 2 is waiting...
Goroutine 2 received the signal.
Goroutine 1 received the signal.
相关文章
|
15天前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
2月前
|
Go 数据库
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
143 11
|
3月前
|
Linux Go iOS开发
怎么禁用 vscode 中点击 go 包名时自动打开浏览器跳转到 pkg.go.dev
本文介绍了如何在 VSCode 中禁用点击 Go 包名时自动打开浏览器跳转到 pkg.go.dev 的功能。通过将 gopls 的 `ui.navigation.importShortcut` 设置为 &quot;Definition&quot;,可以实现仅跳转到定义处而不打开链接。具体操作步骤包括:打开设置、搜索 gopls、编辑 settings.json 文件并保存更改,最后重启 VSCode 使设置生效。
108 7
怎么禁用 vscode 中点击 go 包名时自动打开浏览器跳转到 pkg.go.dev
|
4月前
|
Go 索引
go语言使用strings包
go语言使用strings包
77 3
|
4月前
|
存储 负载均衡 监控
如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
在数字化时代,构建高可靠性服务架构至关重要。本文探讨了如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
96 1
|
4月前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
3月前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
4月前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####
|
4月前
|
Go 调度 开发者
Go语言中的并发编程:深入理解goroutines和channels####
本文旨在探讨Go语言中并发编程的核心概念——goroutines和channels。通过分析它们的工作原理、使用场景以及最佳实践,帮助开发者更好地理解和运用这两种强大的工具来构建高效、可扩展的应用程序。文章还将涵盖一些常见的陷阱和解决方案,以确保在实际应用中能够避免潜在的问题。 ####
|
4月前
|
算法 安全 程序员
Go语言的并发编程:深入理解与实践####
本文旨在探讨Go语言在并发编程方面的独特优势及其实现机制,通过实例解析关键概念如goroutine和channel,帮助开发者更高效地利用Go进行高性能软件开发。不同于传统的摘要概述,本文将以一个简短的故事开头,引出并发编程的重要性,随后详细阐述Go语言如何简化复杂并发任务的处理,最后通过实际案例展示其强大功能。 --- ###