This page looks best with JavaScript enabled

Condition Variables: 搞明白 go 语言的 sync.Cond

 ·  ☕ 3 min read

条件变量(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
66
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
23
❯ 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

Share on

EXEC
WRITTEN BY
EXEC
Evil EXEC