Golang 标准库中提供了sync.Mutex 用于多线程之间的同步。
同时也提供了条件变量配合Mutex结合使用。
条件变量主要的使用场景是:
当线程要执行A操作时,条件B未满足,而无法执行A操作,此时使当前线程进入睡眠状态(进入睡眠状态前会释放锁),等待条件B,当条件B出现时,发送通知唤醒因等待条件B而进入沉睡的线程。
实例说明
一条专门用于读取文件A的线程T1, 一条专门用于写入文件A的线程T2。通过锁M1做读写的同步操作。当T1获取锁M1时,读取文件A,当读取到EOF时,不希望就此结束,而是希望等待,当T2向文件A写入内容后,又可以继续读出内容。此时T1应当释放锁并且通过条件变量进入睡眠状态,然后T2获取锁,对文件进行写入。写完之后释放锁,并且发送一个通知用于唤醒T1,T1被唤醒后,试图获取锁,获取成功后继续进行读操作。
上面的例子中,通过条件变量唤醒睡眠的线程,有两种方式,Signal和Broadcast,如果是Signal会在因当前条件变量而进入睡眠的线程中随机选取一条线程唤醒,然后该线程试图获取锁,如果获取成功,则执行之后的代码逻辑,如果未获取成功则会一直等待,直到获取锁。如果是Broadcast,会将所有因当前条件变量而进入睡眠的线程全部唤醒,所有的线程一起去试图获取锁,哪一条线程先获取到,哪一条线程先执行,其余的线程则继续等待,直到上一次抢到锁的线程释放锁时,再一次开始对于锁的争抢。(注意这里的线程被条件变量唤醒之后,即使未抢到锁,也不再需要条件变量对其进行再一次的通知唤醒)
下面通过代码后打印结果来更深刻的体会
package main
import (
"sync"
"fmt"
"time"
"math/rand"
)
type T struct {
l *sync.Mutex // 锁
c *sync.Cond //条件变量
}
func main() {
wg := sync.WaitGroup{}
wg.Add(1)
var t *T = new(T)
t.l = new(sync.Mutex)
// 使用条件变量前,必须将其与一个锁绑定
t.c = sync.NewCond(t.l)
//启动10条协程,当协程执行conditions()发现满足条件时,打印出processed,代表该协程处理结束
//当不满足条件时,通过条件变量进入睡眠状态,每次从睡眠状态醒来并且获取锁之后,打印出一条wait。
for i := 0; i < 10; i++ {
go func() {
t.l.Lock()
defer t.l.Unlock()
//不满足条件时通过条件变量进入沉入
//t.c.Wait() 首先会释放与该条件变量绑定的锁,然后在进入睡眠状态
for !conditions() {
t.c.Wait()
fmt.Println("wait")
}
fmt.Println("processed")
}()
}
// 启动一条协程 每隔两秒发送一次通知
go func() {
for {
time.Sleep(time.Second * 2)
//fmt.Println("\n\n\n\nBroadcast")
//t.c.Broadcast()
fmt.Println("\n\n\n\nSignal")
t.c.Signal()
}
}()
wg.Wait()
}
//生成随机数, 当生成的数为0时,则为满足条件返回true
func conditions() bool {
i := rand.Intn(10)
fmt.Println(i)
if i == 0 {
return true
} else {
return false
}
}
打印结果
10条协程,在第一次执行时,两条随机出了0,处理完毕,剩余8条协程,通过条件变量进入睡眠状态。
1
7
7
9
1
8
5
0
processed
6
0
processed
过两秒之后发出第一次广播,其中一条协程获取了锁,并且打印了wait进入conditions()随机出了7,为满足条件继续通过条件变量进入睡眠。
Signal
wait
7
同上
Signal
wait
8
再一次唤醒协程,此次conditions()随机出了0满足了条件,打印出processed,处理完毕。
Signal
wait
0
processed
从上面的打印结果可以看出每一次只唤醒了一条携程
接下来将代码中的
fmt.Println("\nSignal")
t.c.Signal()
替换为
fmt.Println("\nBroadcast")
t.c.Broadcast()
再来打印输出结果进行分析
第一次打印出的结果和上面的一样主要看发送广播时的输出
1
7
7
9
1
8
5
0
processed
6
0
processed
停了两秒发送了广播,此时所有关联在条件变量的协程都被唤醒。
从结果可以看出,只发送了一次唤醒广播,所有的协程都打印了一遍wait,说明这些协程都被唤醒,并且依次获取了锁,然后执行。当然一个时间段内只能有一个协程获取到锁。
Broadcast
wait
7
wait
8
wait
0
processed
wait
5
wait
1
wait
8
wait
7
wait
1