GosyncCond,最被忽视的同步机制
来源:dev.to
时间:2024-11-10 14:40:08 460浏览 收藏
偷偷努力,悄无声息地变强,然后惊艳所有人!哈哈,小伙伴们又来学习啦~今天我将给大家介绍《GosyncCond,最被忽视的同步机制》,这篇文章主要会讲到等等知识点,不知道大家对其都有多少了解,下面我们就一起来看一吧!当然,非常希望大家能多多评论,给出合理的建议,我们一起学习,一起进步!
这是帖子的摘录;完整的帖子可以在这里找到: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() {}
好吧,让我们看一个快速的伪示例。这次,我们有一个 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 < 100; i++ { time.sleep(time.millisecond) cond.l.lock() pokemon = pokemonlist[rand.intn(len(pokemonlist))] cond.l.unlock() cond.signal() } }() time.sleep(100 * time.millisecond) // lazy wait } // output: // caught pikachu
在此示例中,一个 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 可能比通道更合适。
- 使用通道,你可以通过发送值的方式向一个 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,最被忽视的同步机制》的详细内容,更多关于的资料请关注golang学习网公众号!
-
505 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
146 收藏
-
184 收藏
-
392 收藏
-
134 收藏
-
188 收藏
-
480 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习