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
类似地,为什么这段代码总是打印相同的结果: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 调度的交互方式。以下是关键点分析:
-
sync.Cond.Wait()的内部机制:
当调用Wait()时,它会将当前 goroutine 加入条件变量的等待队列(通常是一个 FIFO 队列),然后释放锁并阻塞。被Broadcast()唤醒时,Wait()会重新获取锁,但唤醒顺序通常与等待队列的入队顺序一致。 -
goroutine 启动顺序:
在subscribe函数中,每个 goroutine 启动后立即调用wg.Done(),然后在持有锁的情况下调用c.Wait()。由于subscribe是顺序调用的(先 “One”,再 “Two”,后 “Three”),且每个 goroutine 在Wait()前会通过wg.Wait()等待前一个 goroutine 启动完成,这确保了 goroutine 进入等待队列的顺序固定为:
goroutine1(打印 “One”)→ goroutine2(打印 “Two”)→ goroutine3(打印 “Three”)。 -
唤醒顺序:
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()
}
输出可能变为 Two、Three、One 或其他顺序,具体取决于延迟时间。
结论:
原代码的输出顺序是确定性的,因为 goroutine 启动和进入 Wait() 的顺序被同步机制严格保证。若需要非确定性输出,需引入随机延迟或打乱 goroutine 启动时序。sync.Cond 的等待队列行为是实现相关的,但当前 Go 标准库的实现遵循 FIFO 顺序。

