这是帖子的摘录;完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-sync-cond/
这篇文章是有关 go 中处理并发的系列文章的一部分:
- gosync.mutex:正常和饥饿模式
- gosync.wtgroup 和对齐问题
- 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() {}
好吧,让我们看一个快速的伪示例。这次,我们有一个 pokémon 主题,假设我们正在等待一个特定的 pokémon,并且我们希望在它出现时通知其他 goroutines。
var pokemonlist = []string{"pikachu", "charmander", "squirtle", "bulbasaur", "jigglypuff"} var cond = sync.newcond(&sync.mutex{}) var pokemon = "" func main() { // consumer go func() { cond.l.lock() defer cond.l.unlock() // waits until pikachu appears for pokemon != "pikachu" { cond.wait() } println("caught" + pokemon) pokemon = "" }() // producer go func() { // every 1ms, a random pokémon appears for i := 0; i <p>在此示例中,一个 goroutine 正在等待皮卡丘出现,而另一个 goroutine(生产者)从列表中随机选择一个神奇宝贝,并在新神奇宝贝出现时向消费者发出信号。</p> <p>当生产者发送信号时,消费者醒来并检查是否出现了正确的神奇宝贝。如果有,我们就捕获神奇宝贝,如果没有,消费者就回去睡觉并等待下一个。</p> <p>问题是,生产者发送信号和消费者实际醒来之间存在差距。与此同时,pokémon 可能会发生变化,因为消费者 goroutine 可能会晚于 1 毫秒(很少)醒来,或者其他 goroutine 会修改共享的 pokemon。所以sync.cond 基本上是在说:<em>'嘿,有些东西改变了!醒过来看看,但如果太晚了,可能又会变了。'</em> </p> <p>如果消费者起晚了,pokémon 可能会逃跑,而 goroutine 会重新进入睡眠状态。</p> <blockquote> <p><strong><em>“哈,我可以使用一个通道来将 pokemon 名称或信号发送给另一个 goroutine”</em></strong></p> </blockquote> <p>当然。事实上,在 go 中,通道通常比sync.cond更受欢迎,因为它们更简单,更惯用,并且为大多数开发人员所熟悉。</p> <p>在上面的情况下,您可以轻松地通过通道发送 pokémon 名称,或者仅使用空 struct{} 来发出信号而不发送任何数据。但我们的问题不仅仅是通过通道传递消息,而是处理共享状态。 </p> <p>我们的例子非常简单,但是如果多个 goroutine 访问共享的 pokemon 变量,让我们看看如果我们使用通道会发生什么:</p>
- 如果我们使用通道发送 pokémon 名称,我们仍然需要一个互斥体来保护共享的 pokemon 变量。
- 如果我们仅使用通道来发出信号,则仍然需要互斥体来管理对共享状态的访问。
- 如果我们在生产者中检查皮卡丘,然后通过通道发送它,我们还需要一个互斥锁。最重要的是,我们违反了关注点分离原则,即生产者承担了真正属于消费者的逻辑。
也就是说,当多个 goroutine 修改共享数据时,仍然需要互斥体来保护它。在这些情况下,您经常会看到通道和互斥体的组合,以确保正确的同步和数据安全。
“好的,但是广播信号呢?”
好问题!您确实可以通过简单地关闭通道(close(ch))来使用通道向所有等待的 goroutine 模仿广播信号。当您关闭通道时,从该通道接收的所有 goroutine 都会收到通知。但请记住,关闭的通道无法重复使用,一旦关闭,它就会保持关闭状态。
顺便说一句,实际上有人在讨论在 go 2 中删除sync.cond:提案:sync:删除 cond 类型。
“那么,sync.cond 有什么用呢?”
嗯,在某些情况下,sync.cond 可能比通道更合适。
- 使用通道,你可以通过发送值的方式向一个 goroutine 发送信号,也可以通过关闭通道来通知所有 goroutine,但你不能同时执行这两种操作。 sync.cond 为您提供更细粒度的控制。你可以调用 signal() 来唤醒单个 goroutine,或者调用 broadcast() 来唤醒所有 goroutine。
- 并且您可以根据需要多次调用 broadcast(),而通道一旦关闭就无法执行此操作(关闭已关闭的通道会引发恐慌)。
- 通道不提供保护共享数据的内置方法 – 您需要使用互斥体单独管理它。另一方面,sync.cond 通过将锁定和信号发送到一个包中,为您提供了一种更加集成的方法(以及更好的性能)。
“为什么要在sync.cond中嵌入lock?”
理论上,像sync.cond 这样的条件变量不必绑定到锁即可使其信号正常工作。
您可以让用户在条件变量之外管理自己的锁,这听起来像是提供了更大的灵活性。这并不是真正的技术限制,更多的是人为错误。
手动管理很容易导致错误,因为该模式不太直观,您必须在调用 wait() 之前解锁互斥体,然后在 goroutine 唤醒时再次锁定它。这个过程可能会让人感觉尴尬,而且很容易出错,比如忘记在正确的时间锁定或解锁。
但是为什么图案看起来有点不对劲?
通常,调用 cond.wait() 的 goroutine 需要在循环中检查某些共享状态,如下所示:
for !checksomesharedstate() { cond.wait() }
sync.cond 中嵌入的锁帮助我们处理锁定/解锁过程,使代码更简洁且不易出错,我们将很快详细讨论该模式。
如何使用?
如果仔细观察前面的示例,您会注意到消费者中的一致模式:我们总是在等待 (.wait()) 条件之前锁定互斥体,并在满足条件后解锁它。
另外,我们将等待条件包装在一个循环中,这里复习一下:
// consumer go func() { cond.l.lock() defer cond.l.unlock() // waits until pikachu appears for pokemon != "pikachu" { cond.wait() } println("caught" + pokemon) }()
条件等待()
当我们在sync.cond 上调用wait() 时,我们是在告诉当前的goroutine 等待,直到满足某些条件。
这是幕后发生的事情:
- 该 goroutine 被添加到其他也在等待相同条件的 goroutine 列表中。所有这些 goroutine 都被阻塞,这意味着它们无法继续,直到被 signal() 或 broadcast() 调用“唤醒”为止。
- 这里的关键部分是,在调用 wait() 之前必须锁定互斥锁,因为 wait() 做了一些重要的事情,它会在让 goroutine 休眠之前自动释放锁(调用 unlock())。这允许其他 goroutine 在原始 goroutine 等待时获取锁并完成其工作。
- 当等待的 goroutine 被唤醒(通过 signal() 或 broadcast())时,它不会立即恢复工作。首先,它必须重新获取锁(lock())。
以下是 wait() 在底层的工作原理:
func (c *cond) wait() { // check if cond has been copied c.checker.check() // get the ticket number t := runtime_notifylistadd(&c.notify) // unlock the mutex c.l.unlock() // suspend the goroutine until being woken up runtime_notifylistwait(&c.notify, t) // re-lock the mutex c.l.lock() }
虽然很简单,但我们可以总结出4个要点:
- 有一个检查器可以防止复制 cond 实例,如果这样做会出现恐慌。
- 调用 cond.wait() 会立即解锁互斥体,因此在调用 cond.wait() 之前必须锁定互斥体,否则会出现恐慌。
- 被唤醒后,cond.wait() 会重新锁定互斥体,这意味着您在使用完共享数据后需要再次解锁它。
- sync.cond 的大部分功能是在 go 运行时中通过名为 notificationlist 的内部数据结构实现的,该结构使用基于票据的系统进行通知。
由于这种锁定/解锁行为,在使用sync.cond.wait() 时您将遵循一个典型模式以避免常见错误:
c.L.Lock() for !condition() { c.Wait() } // ... make use of condition ... c.L.Unlock()
“为什么不直接使用 c.wait() 而不使用循环呢?”
这是帖子的摘录;完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-sync-cond/
以上就是GosyncCond,最被忽视的的详细内容,更多请关注php中文网其它相关文章!