这是帖子的摘录;完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-sync-cond/
这篇文章是关于 Go 中处理并发的系列文章的一部分:
在Go中,sync.Cond是一个同步原语,尽管它不像sync.Mutex或sync.WaitGroup那样常用。您很少会在大多数项目中甚至在标准库中看到它,而其他同步机制往往会取代它。
也就是说,作为一名 Go 工程师,你不会真的希望自己在阅读使用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 协调工作的更好方法。从技术上讲,如果您来自更学术的背景,那么它是一个“条件变量”。
这是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。
// 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 变量,让我们看看如果我们使用通道会发生什么:
也就是说,当多个 goroutine 修改共享数据时,仍然需要互斥体来保护它。在这些情况下,您经常会看到通道和互斥体的组合,以确保正确的同步和数据安全。
“好的,但是广播信号呢?”
好问题!您确实可以通过简单地关闭通道(close(ch))来使用通道向所有等待的 goroutine 模仿广播信号。当您关闭通道时,从该通道接收的所有 goroutine 都会收到通知。但请记住,关闭的通道无法重复使用,一旦关闭,它就会保持关闭状态。
顺便说一句,实际上有人在谈论 Go 2 中删除sync.Cond:提案:sync:删除 Cond 类型。
“那么,sync.Cond 有什么用呢?”
嗯,在某些情况下,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 坚持下去,直到满足某些条件。
这是幕后发生的事情:
以下是 Wait() 在底层的工作原理:
// wait until condition is true for !condition { } // or for !condition { time.Sleep(100 * time.Millisecond) }
虽然很简单,但我们可以总结出4个要点:
由于这种锁定/解锁行为,在使用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() {}
“为什么不直接使用 c.Wait() 而不使用循环呢?”
这是帖子的摘录;完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-sync-cond/
以上是Gosync.Cond,最被忽视的同步机制的详细内容。更多信息请关注PHP中文网其他相关文章!