Golang中如何同步这段代码

Golang中如何同步这段代码 我运行了这段代码,它总是以相同的顺序输出(三、一、二)。为什么会这样?难道不应该是非确定性的吗?

package main

import (
    "fmt"
    "sync"
)

type Button struct {
    Clicked *sync.Cond
}

func subscribe(c *sync.Cond, fn func()) {
    var wg sync.WaitGroup
    wg.Add(1)

    go func() {
        wg.Done()

        c.L.Lock()
        defer c.L.Unlock()

        c.Wait()
        fn()
    }()

    wg.Wait()
}

func main() {
    button := Button{Clicked: sync.NewCond(&sync.Mutex{})}

    var wg sync.WaitGroup
    wg.Add(3)

    subscribe(button.Clicked, func() {
        fmt.Println("One")

        wg.Done()
    })

    subscribe(button.Clicked, func() {
        fmt.Println("Two")

        wg.Done()
    })

    subscribe(button.Clicked, func() {
        fmt.Println("Three")

        wg.Done()
    })

    button.Clicked.Broadcast()
    wg.Wait()
}

你们怎么看?


更多关于Golang中如何同步这段代码的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

类似地,为什么这段代码总是打印相同的结果:https://play.golang.org/p/mbCIa-F7Vmz

这怎么可能,是什么在同步这两个 goroutine?

更多关于Golang中如何同步这段代码的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


请不要忘记,Playground 会缓存代码的输出结果……

如果我在本地编译并运行这段代码,每次得到的答案并不相同

$ go build z.go
$ ./z
def
abc
$ ./z
def
abc
$ ./z
abc
def
$ ./z
def
abc
$ ./z
abc
def

JOhn_Stuart:

你怎么看?

我认为每次可能有3个goroutine按顺序被添加到sync.Cond的等待列表中,所以当你运行button.Clicked.Broadcast()时,它们总是以相同的顺序运行。

Broadcast没有指定任何关于顺序的内容,所以我推测它只是遍历切片,依次唤醒每个等待的goroutine。

func (c *Cond) Broadcast() Broadcast唤醒所有在c上等待的goroutine。

调用者在调用期间持有c.L是允许的,但不是必需的。

这段代码的输出顺序是确定性的,原因在于 sync.Cond.Wait() 的实现机制和 goroutine 调度的交互方式。以下是关键点分析:

  1. sync.Cond.Wait() 的内部机制
    当调用 Wait() 时,它会将当前 goroutine 加入条件变量的等待队列(通常是一个 FIFO 队列),然后释放锁并阻塞。被 Broadcast() 唤醒时,Wait() 会重新获取锁,但唤醒顺序通常与等待队列的入队顺序一致

  2. goroutine 启动顺序
    subscribe 函数中,每个 goroutine 启动后立即调用 wg.Done(),然后在持有锁的情况下调用 c.Wait()。由于 subscribe 是顺序调用的(先 “One”,再 “Two”,后 “Three”),且每个 goroutine 在 Wait() 前会通过 wg.Wait() 等待前一个 goroutine 启动完成,这确保了 goroutine 进入等待队列的顺序固定为:
    goroutine1(打印 “One”)→ goroutine2(打印 “Two”)→ goroutine3(打印 “Three”)。

  3. 唤醒顺序
    Broadcast() 会唤醒所有等待的 goroutine,但它们在重新获取锁时会按进入等待队列的顺序依次执行。由于每个 goroutine 在 Wait() 前已持有锁,且唤醒后需重新获取同一把锁,因此执行顺序与入队顺序相同。

示例验证
以下代码通过添加延迟来破坏顺序,展示非确定性输出:

package main

import (
    "fmt"
    "sync"
    "time"
)

type Button struct {
    Clicked *sync.Cond
}

func subscribe(c *sync.Cond, fn func(), delay time.Duration) {
    var wg sync.WaitGroup
    wg.Add(1)

    go func() {
        wg.Done()
        time.Sleep(delay) // 人为延迟,影响入队顺序

        c.L.Lock()
        defer c.L.Unlock()

        c.Wait()
        fn()
    }()

    wg.Wait()
}

func main() {
    button := Button{Clicked: sync.NewCond(&sync.Mutex{})}

    var wg sync.WaitGroup
    wg.Add(3)

    subscribe(button.Clicked, func() {
        fmt.Println("One")
        wg.Done()
    }, 0)

    subscribe(button.Clicked, func() {
        fmt.Println("Two")
        wg.Done()
    }, 100*time.Millisecond) // 延迟启动

    subscribe(button.Clicked, func() {
        fmt.Println("Three")
        wg.Done()
    }, 50*time.Millisecond) // 延迟启动

    time.Sleep(200 * time.Millisecond) // 确保所有 goroutine 已进入等待
    button.Clicked.Broadcast()
    wg.Wait()
}

输出可能变为 TwoThreeOne 或其他顺序,具体取决于延迟时间。

结论
原代码的输出顺序是确定性的,因为 goroutine 启动和进入 Wait() 的顺序被同步机制严格保证。若需要非确定性输出,需引入随机延迟或打乱 goroutine 启动时序。sync.Cond 的等待队列行为是实现相关的,但当前 Go 标准库的实现遵循 FIFO 顺序。

回到顶部