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.
到此这篇关于Go语言并发之Sync包的6个关键概念总结的文章就介绍到这了,更多相关Go语言Sync包内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!