首页 > 后端开发 > Golang > 正文

Gosync.Cond,最被忽视的同步机制

DDD
发布: 2024-10-30 06:43:28
原创
299 人浏览过

这是帖子的摘录;完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-sync-cond/

这篇文章是关于 Go 中处理并发的系列文章的一部分:

  • Gosync.Mutex:正常和饥饿模式
  • Gosync.WaitGroup 和对齐问题
  • Gosync.Pool 及其背后的机制
  • 使用sync.Cond,最被忽视的同步机制(我们来了)
  • Gosync.Map:适合正确工作的正确工具
  • Go Singleflight 融入您的代码,而不是您的数据库

在Go中,sync.Cond是一个同步原语,尽管它不像sync.Mutex或sync.WaitGroup那样常用。您很少会在大多数项目中甚至在标准库中看到它,而其他同步机制往往会取代它。

也就是说,作为一名 Go 工程师,你不会真的希望自己在阅读使用sync.Cond 的代码时却不知道发生了什么,因为毕竟它是标准库的一部分。

因此,本次讨论将帮助您缩小这一差距,更好的是,它会让您更清楚地了解它在实践中的实际运作方式。

什么是sync.Cond?

那么,让我们来分析一下sync.Cond 的意义。

当 goroutine 需要等待特定事情发生时,例如某些共享数据更改,它可以“阻塞”,这意味着它只是暂停其工作,直到获得继续的许可。最基本的方法是使用循环,甚至可能添加一个 time.Sleep 来防止 CPU 因忙等待而疯狂。

这可能是这样的:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登录后复制
登录后复制
登录后复制
登录后复制

现在,这并不是真正有效,因为该循环仍在后台运行,消耗 CPU 周期,即使没有任何更改。

这就是sync.Cond 发挥作用的地方,这是让 goroutine 协调工作的更好方法。从技术上讲,如果您来自更学术的背景,那么它是一个“条件变量”。

  • 当一个goroutine正在等待某件事发生时(等待某个条件成立),它可以调用Wait()。
  • 另一个 Goroutine,一旦知道条件可能满足,就可以调用 Signal() 或 Broadcast() 来唤醒等待的 Goroutine,并让它们知道是时候继续前进了。

这是sync.Cond的基本接口:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
登录后复制
登录后复制
登录后复制

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond 概述

好吧,让我们看一个快速的伪示例。这次,我们有一个 Pokémon 主题,假设我们正在等待一个特定的 Pokémon,并且我们希望在它出现时通知其他 Goroutines。

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登录后复制
登录后复制
登录后复制
登录后复制

在此示例中,一个 Goroutine 正在等待皮卡丘出现,而另一个 Goroutine(生产者)从列表中随机选择一个神奇宝贝,并在新神奇宝贝出现时向消费者发出信号。

当生产者发送信号时,消费者醒来并检查是否出现了正确的神奇宝贝。如果有,我们就捕获神奇宝贝,如果没有,消费者就回去睡觉并等待下一个。

问题是,生产者发送信号和消费者实际醒来之间存在差距。与此同时,Pokémon 可能会发生变化,因为消费者 Goroutine 可能会晚于 1 毫秒(很少)醒来,或者其他 Goroutine 会修改共享的 Pokemon。所以sync.Cond 基本上是在说:'嘿,有些东西改变了!醒过来看看,但如果太晚了,可能又会变了。'

如果消费者起晚了,Pokémon 可能会逃跑,而 Goroutine 会重新进入睡眠状态。

“嗯,我可以使用一个通道来将 Pokemon 名称或信号发送给另一个 Goroutine”

当然。事实上,在 Go 中,通道通常比sync.Cond更受欢迎,因为它们更简单,更惯用,并且为大多数开发人员所熟悉。

在上面的情况下,您可以轻松地通过通道发送 Pokémon 名称,或者仅使用空 struct{} 来发出信号而不发送任何数据。但我们的问题不仅仅是通过通道传递消息,还涉及处理共享状态。

我们的例子非常简单,但是如果多个 goroutine 访问共享的 pokemon 变量,让我们看看如果我们使用通道会发生什么:

  • 如果我们使用通道发送 Pokémon 名称,我们仍然需要一个互斥体来保护共享的 pokemon 变量。
  • 如果我们仅使用通道来发出信号,则仍然需要互斥体来管理对共享状态的访问。
  • 如果我们在生产者中检查皮卡丘,然后通过通道发送它,我们还需要一个互斥体。最重要的是,我们违反了关注点分离原则,即生产者承担了真正属于消费者的逻辑。

也就是说,当多个 goroutine 修改共享数据时,仍然需要互斥体来保护它。在这些情况下,您经常会看到通道和互斥体的组合,以确保正确的同步和数据安全。

“好的,但是广播信号呢?”

好问题!您确实可以通过简单地关闭通道(close(ch))来使用通道向所有等待的 goroutine 模仿广播信号。当您关闭通道时,从该通道接收的所有 goroutine 都会收到通知。但请记住,关闭的通道无法重复使用,一旦关闭,它就会保持关闭状态。

顺便说一句,实际上有人在谈论 Go 2 中删除sync.Cond:提案:sync:删除 Cond 类型。

“那么,sync.Cond 有什么用呢?”

嗯,在某些情况下,sync.Cond 可能比通道更合适。

  1. 使用通道,你可以通过发送值的方式向一个 goroutine 发送信号,也可以通过关闭通道来通知所有 goroutine,但你不能同时执行这两种操作。 sync.Cond 为您提供更细粒度的控制。你可以调用 Signal() 来唤醒单个 goroutine,或者调用 Broadcast() 来唤醒所有 goroutine。
  2. 并且您可以根据需要多次调用 Broadcast(),而通道一旦关闭就无法执行此操作(关闭已关闭的通道会引发恐慌)。
  3. 通道不提供保护共享数据的内置方法 - 您需要使用互斥体单独管理它。另一方面,sync.Cond 通过将锁定和信号发送到一个包中,为您提供了一种更加集成的方法(以及更好的性能)。

“为什么要在sync.Cond中嵌入Lock?”

理论上,像sync.Cond 这样的条件变量不必绑定到锁即可使其信号正常工作。

您可以让用户在条件变量之外管理自己的锁,这听起来像是提供了更大的灵活性。这并不是真正的技术限制,而更多的是人为错误。

手动管理很容易导致错误,因为该模式不太直观,您必须在调用 Wait() 之前解锁互斥体,然后在 goroutine 唤醒时再次锁定它。这个过程可能会让人感觉尴尬,而且很容易出错,比如忘记在正确的时间锁定或解锁。

但是为什么图案看起来有点不对劲?

通常,调用 cond.Wait() 的 goroutine 需要在循环中检查某些共享状态,如下所示:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登录后复制
登录后复制
登录后复制
登录后复制

sync.Cond 中嵌入的锁帮助我们处理锁定/解锁过程,使代码更简洁且不易出错,我们很快就会详细讨论该模式。

如何使用?

如果仔细观察前面的示例,您会注意到消费者中的一致模式:我们总是在等待(.Wait())条件之前锁定互斥体,并在满足条件后解锁它。

另外,我们将等待条件包装在一个循环中,这里复习一下:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
登录后复制
登录后复制
登录后复制

条件等待()

当我们在sync.Cond 上调用Wait() 时,我们是在告诉当前的goroutine 坚持下去,直到满足某些条件。

这是幕后发生的事情:

  1. 该 goroutine 被添加到其他也在等待相同条件的 goroutine 列表中。所有这些 goroutine 都被阻塞,这意味着它们无法继续,直到被 Signal() 或 Broadcast() 调用“唤醒”。
  2. 这里的关键部分是,在调用 Wait() 之前必须锁定互斥锁,因为 Wait() 做了一些重要的事情,它会在让 goroutine 休眠之前自动释放锁(调用 Unlock())。这允许其他 Goroutine 在原始 Goroutine 等待时获取锁并完成其工作。
  3. 当等待的 goroutine 被唤醒(通过 Signal() 或 Broadcast())时,它不会立即恢复工作。首先,它必须重新获取锁(Lock())。

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond.Wait() 方法

以下是 Wait() 在底层的工作原理:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
登录后复制
登录后复制
登录后复制
登录后复制

虽然很简单,但我们可以总结出4个要点:

  1. 有一个检查器可以防止复制 Cond 实例,如果这样做会出现恐慌。
  2. 调用 cond.Wait() 会立即解锁互斥体,因此在调用 cond.Wait() 之前必须锁定互斥体,否则会出现恐慌。
  3. 被唤醒后,cond.Wait() 会重新锁定互斥体,这意味着您在使用完共享数据后需要再次解锁它。
  4. sync.Cond 的大部分功能是在 Go 运行时中通过名为 notificationList 的内部数据结构实现的,该结构使用基于票据的系统进行通知。

由于这种锁定/解锁行为,在使用sync.Cond.Wait() 时您将遵循一个典型模式以避免常见错误:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
登录后复制
登录后复制
登录后复制

Go sync.Cond, the Most Overlooked Sync Mechanism

使用sync.Cond.Wait()的典型模式

“为什么不直接使用 c.Wait() 而不使用循环呢?”


这是帖子的摘录;完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-sync-cond/

以上是Gosync.Cond,最被忽视的同步机制的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板