找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1163

积分

0

好友

163

主题
发表于 13 小时前 | 查看: 1| 回复: 0

在 Go 并发编程实践中,有时会遇到一些看似违反直觉的现象。本文将通过一个经典的算法面试题案例,深入探讨 Go 内存模型中的核心规则——Happens-Before 原则,并揭示一个容易被忽视的并发陷阱。

代码演示:预期的同步与意外的结果

下面的代码试图利用无缓冲 Channel 的同步特性,让两个 Goroutine 交替打印 “f” 和 “b”。

// 交替打印 f b
type FB struct {
    n int
    f chan bool
}

func f1() {
    fb := FB{n: 10, f: make(chan bool)}
    // G1: 先发送信号
    go func() {
        for i := 0; i < fb.n; i++ {
            fb.f <- true
            fmt.Println("f")
        }
    }()
    // G2: 先接收信号
    go func() {
        for i := 0; i < fb.n; i++ {
            <-fb.f
            fmt.Println("b")
        }
    }()
    time.Sleep(time.Second)
}

关键问题:你认为这段代码的输出顺序是什么?

直观来看,由于无缓冲 Channel 的发送和接收操作是同步的,G1 发送一个值,G2 接收一个值,两者会互相等待。因此,很多人会预期输出是严格交替的:f b f b f b ...

然而,实际多次运行此程序后,你可能会观察到诸如 f f b b f b 这样的输出序列。这似乎表明,Goroutine 在对方尚未完成打印操作时,就抢先执行了自身的打印。这不禁让人困惑:Channel 的同步作用去哪儿了?

解密时刻:理解 Happens-Before 原则

要透彻理解这个问题,需要引入 Go 并发编程的基石——Happens-Before 原则。它定义了在并发执行中,操作之间的偏序关系,即哪些操作必须在哪些操作之前完成。

我们来分析上述代码中哪些顺序是“必然”的,哪些是“偶然”的。

必然的同步链

根据 Go 语言规范,对于无缓冲 Channel 或已满的有缓冲 Channel,一次发送操作完成之前,对应的接收操作必须已经开始。 反之,对于非空的有缓冲 Channel 的接收操作,也必须在对应的发送操作完成之前开始。

在我们的例子中,每一轮循环都构建了一条确定的同步链

  1. G1 执行发送操作 fb.f <- true
  2. 该操作与 G2 的接收操作 <-fb.f 同步(握手成功)。
  3. G2 的接收操作完成并从该语句返回。

这意味着一个关键事实:当 G2 从 <-fb.f 语句返回时,G1 对应的 fb.f <- true 操作必定已经发生并完成。 这是由语言规范保证的确定顺序。

“失控”的后续操作

然而,Channel 的同步作用范围是有限的,它仅仅保障了发送接收这两个动作本身的先后顺序。一旦这次 Channel 握手完成,两个 Goroutine 便分道扬镳,进入“自由竞争”状态:

  • G1 的后续逻辑是:执行 fmt.Println("f")
  • G2 的后续逻辑是:执行 fmt.Println("b")

那么问题来了:G1 的打印操作和 G2 的打印操作,谁先执行?

答案是:无法确定!

根据 Go 的内存模型,这两个 Println 操作之间不存在任何 Happens-Before 关系。它们是完全并发(Concurrent)的。因此,它们的执行顺序完全交由 Go 调度器决定:

  • 如果 G1 所在的系统线程时间片先到,就可能先打印 f
  • 如果 G2 先被调度执行,就会先打印 b

这就是输出出现乱序的根本原因。Channel 像一把发令枪,枪响(同步点完成)之后,两个“运动员”(G1 和 G2)谁跑得快(谁先被调度执行后续代码),发令枪是管不了的。

核心结论与解决方案

  1. 同步范围有限:Channel 仅保证数据传递(发送与接收)这一瞬间的同步,不保证其前后其他操作(如打印、计算)的顺序。
  2. 调度非确定性:Goroutine 的调度是非确定性的,同一段并发代码多次运行可能产生不同的交织顺序,这是正常现象。
  3. 严格顺序控制:若需保证严格的交替执行顺序(如必须先打印完 “f” 才能打印 “b”),需要引入额外的同步机制。例如,可以使用两个 Channel 形成“接力”,或者使用 sync.Mutex 在打印操作上加锁。

理解并正确应用 Happens-Before 原则,是编写正确、可靠并发程序的关键。这能帮助你避免陷入“以为同步了,实际却乱了”的思维陷阱。




上一篇:视频播放器氛围模式实现:基于Android/iOS的动态主色调填充技术
下一篇:NVIDIA Merlin AI框架反序列化漏洞分析:NVTabular与Transformers4Rec组件存在RCE风险
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 15:14 , Processed in 0.118070 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表