条件变量(Condition VAriables)是一种并发原语,条件变量允许一个线程在某个条件不满足的时候进入睡眠状态,当条件满足时再唤醒它。条件变量还支持当条件满足时,唤醒一个(wake one)和唤醒所有(wake all)。
我想出来了一个例子
我方有 10 架雷达,可以发现 0~300km 之内的敌机,但只有 3 台地下导弹发射井,分别可以打击 0~100km, 100km~200km, 200km~300km 范围内的目标:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
func TestCondAir(t *testing.T) {
c := sync.NewCond(&sync.Mutex{})
position := int32(-1)
for person := 0; person < 10; person++ {
go func(person int) {
intn := rand.Intn(100)
if intn < 0 {
intn *= -1
}
duration := time.Duration(intn) * time.Millisecond
time.Sleep(duration)
c.L.Lock()
position = rand.Int31n(300)
c.L.Unlock()
t.Logf("%2d 号雷达发现了敌方侦察机,位置在%3d", person, position)
c.Broadcast()
}(person)
}
go func() {
for {
c.L.Lock()
for position >= 100 || position < 0 { // [0,100)
c.Wait()
}
t.Logf("射程为 100m 的导弹发射被唤醒,井发射导弹位置%3d\n", position)
position = -1
c.L.Unlock()
}
}()
go func() {
for {
c.L.Lock()
for position < 100 || position >= 200 { // [100,200)
c.Wait()
}
t.Logf("射程为 200m 的导弹发射被唤醒,井发射导弹, 位置%3d\n", position)
position = -1
c.L.Unlock()
}
}()
go func() {
for {
c.L.Lock()
for position < 200 { // [200,300]
c.Wait()
}
t.Logf("射程为 300m 的导弹发射被唤醒,井发射导弹, 位置%3d\n", position)
position = -1
c.L.Unlock()
}
}()
select {
}
}
|
测试的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
❯ go test -v . -count=1 -run TestCondAir
=== RUN TestCondAir
sync.cond_test.go:35: 7 号雷达发现了敌机,位置在 57km
sync.cond_test.go:47: 射程为 100km 的导弹发射井被唤醒,井发射导弹, 位置 57km
sync.cond_test.go:35: 2 号雷达发现了敌机,位置在 166km
sync.cond_test.go:61: 射程为 200km 的导弹发射井被唤醒,井发射导弹, 位置 166km
sync.cond_test.go:35: 3 号雷达发现了敌机,位置在 63km
sync.cond_test.go:47: 射程为 100km 的导弹发射井被唤醒,井发射导弹, 位置 63km
sync.cond_test.go:35: 5 号雷达发现了敌机,位置在 98km
sync.cond_test.go:47: 射程为 100km 的导弹发射井被唤醒,井发射导弹, 位置 98km
sync.cond_test.go:35: 4 号雷达发现了敌机,位置在 232km
sync.cond_test.go:74: 射程为 300km 的导弹发射井被唤醒,井发射导弹, 位置 232km
sync.cond_test.go:35: 6 号雷达发现了敌机,位置在 153km
sync.cond_test.go:61: 射程为 200km 的导弹发射井被唤醒,井发射导弹, 位置 153km
sync.cond_test.go:35: 1 号雷达发现了敌机,位置在 125km
sync.cond_test.go:61: 射程为 200km 的导弹发射井被唤醒,井发射导弹, 位置 125km
sync.cond_test.go:35: 0 号雷达发现了敌机,位置在 238km
sync.cond_test.go:74: 射程为 300km 的导弹发射井被唤醒,井发射导弹, 位置 238km
sync.cond_test.go:35: 9 号雷达发现了敌机,位置在 106km
sync.cond_test.go:61: 射程为 200km 的导弹发射井被唤醒,井发射导弹, 位置 106km
sync.cond_test.go:35: 8 号雷达发现了敌机,位置在 22km
sync.cond_test.go:47: 射程为 100km 的导弹发射井被唤醒,井发射导弹, 位置 22km
|
虚假唤醒:Spurious Wakeup
Spurious wakeup describes a complication in the use of condition variables as provided by certain multithreading APIs such as POSIX Threads and the Windows API. Even after a condition variable appears to have been signaled from a waiting thread’s point of view, the condition that was awaited may still be false.
https://en.wikipedia.org/wiki/Spurious_wakeup
虚假唤醒是指 Wait 有可能会在条件变量不被满足的情况下返回, 所以得在 while loop 里检查条件变量。
chromium 官方文档里关于 condvars 的说明
https://www.chromium.org/developers/lock-and-condition-variable
关于 sync.Cond 的讨论
https://github.com/golang/go/issues/21165
有人提议:要在 Go 2 移除 sync.Cond,他认为(*sync.Cond).Broadcast 完全课程被 channel 的 close 取代,而 signal 可以被 channel send 取代。
虽然在某些场景下 channel 可以替代 sync.Cond, 但是 channel 关闭之后就不能被再次打开了。而 syhnc.Cond 可以任意 Broadcast()
sync.Cond 源码
Cond 里有个 Locker 和 notiflist,
1
2
3
4
5
6
7
8
9
|
type Cond struct {
noCopy noCopy // 保证 Cond 不被复制
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
}
|
其中 notifyList 是最重要的,它维护了等待唤醒的 goroutine,是个链表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// notifyList is a ticket-based notification list used to implement sync.Cond.
//
// It must be kept in sync with the sync package.
type notifyList struct {
// wait is the ticket number of the next waiter. It is atomically
// incremented outside the lock.
wait uint32
// notify is the ticket number of the next waiter to be notified. It can
// be read outside the lock, but is only written to with lock held.
//
// Both wait & notify can wrap around, and such cases will be correctly
// handled as long as their "unwrapped" difference is bounded by 2^31.
// For this not to be the case, we'd need to have 2^31+ goroutines
// blocked on the same condvar, which is currently not possible.
notify uint32
// List of parked waiters.
lock mutex
head *sudog
tail *sudog
}
|
Reference
https://docs.microsoft.com/en-us/windows/win32/sync/using-condition-variables
https://en.wikipedia.org/wiki/Spurious_wakeup
https://www.chromium.org/developers/lock-and-condition-variable
https://github.com/golang/go/issues/21165