资源竞争
可以使用 sync.Mutex 互斥锁
以上被加锁保护的 sum+=i 代码片段又称为临界区。
这个示例开启了 10 个协程,它们同时读取 sum 的值。因为 readSum 函数并没有任何加锁控制,所以它不是并发安全的,即一个 goroutine 正在执行 sum+=i 操作的时候,另一个 goroutine 可能正在执行 b:=sum 操作,这就会导致读取的 num 值是一个过期的值,结果不可预期。
互斥锁sync.Mutex
如果要解决以上资源竞争的问题,可以使用互斥锁 sync.Mutex,那么有没有办法监听所有协程的执行,一旦全部执行完毕,程序马上退出,Go 提供了更简洁的解决办法,它就是 sync.WaitGroup
//共享的资源
var(
sum int
mutex sync.Mutex
mutexr sync.RWMutex
)
func add(i int) {
mutex.Lock()
defer mutex.Unlock()
sum += i
}
func run(){
var wg sync.WaitGroup
//因为要监控110个协程,所以设置计数器为110
wg.Add(110)
for i := 0; i < 100; i++ {
go func() {
//计数器值减1
defer wg.Done()
add(10)
}()
}
for i:=0; i<10;i++ {
go func() {
//计数器值减1
defer wg.Done()
fmt.Println("和为:",readSum())
}()
}
//一直等待,只要计数器值为0
wg.Wait()
}
func main() {
run()
//和为: 1000
//和为: 1000
//和为: 1000
//和为: 1000
//和为: 1000
//和为: 1000
//和为: 1000
//和为: 1000
//和为: 1000
//和为: 1000
}
//增加了一个读取sum的函数,便于演示并发
func readSum() int {
//只获取读锁
mutexr.RLock()
defer mutexr.RUnlock()
b:=sum
return b
}
sync.WaitGroup 一共分为三步:
- 声明一个 sync.WaitGroup,然后通过 Add 方法设置计数器的值,需要跟踪多少个协程就设置多少,这里是 110
- 在每个协程执行完毕时调用 Done 方法,让计数器减 1,告诉 sync.WaitGroup 该协程已经执行完毕
- 最后调用 Wait 方法一直等待,直到计数器值为 0,也就是所有跟踪的协程都执行完毕
sync.WaitGroup 适合协调多个协程共同做一件事情的场景,比如下载一个文件,假设使用 10 个协程,每个协程下载文件的 1/10 大小,只有 10 个协程都下载好了整个文件才算是下载好了。这就是我们经常听到的多线程下载,通过多个线程共同做一件事情,显著提高效率。
Go 提供了 sync.Once 来保证代码只执行一次
func main() {
doOnce()
}
func doOnce() {
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
//用于等待协程执行完毕
done := make(chan bool)
//启动10个协程执行once.Do(onceBody)
for i := 0; i < 10; i++ {
go func() {
//把要执行的函数(方法)作为参数传给once.Do方法即可
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
10 个协程来执行 onceBody 函数,但是因为用了 once.Do 方法,所以函数 onceBody 只会被执行一次。也就是说在高并发的情况下,sync.Once 也会保证 onceBody 函数只执行一次。
//10个人赛跑,1个裁判发号施令
func race(){
cond :=sync.NewCond(&sync.Mutex{})
var wg sync.WaitGroup
wg.Add(11)
for i:=0;i<10; i++ {
go func(num int) {
defer wg.Done()
fmt.Println(num,"号已经就位")
cond.L.Lock()
cond.Wait()//等待发令枪响
fmt.Println(num,"号开始跑……")
cond.L.Unlock()
}(i)
}
//等待所有goroutine都进入wait状态
time.Sleep(2*time.Second)
go func() {
defer wg.Done()
fmt.Println("裁判已经就位,准备发令枪")
fmt.Println("比赛开始,大家准备跑")
cond.Broadcast()//发令枪响
}()
//防止函数提前返回退出
wg.Wait()
}
sync.Cond 可以用于发号施令
在 Go 语言中,sync.WaitGroup 用于最终完成的场景,关键点在于一定要等待所有协程都执行完毕。而 sync.Cond 可以用于发号施令,一声令下所有协程都可以开始执行,关键点在于协程开始的时候是等待的,要等待 sync.Cond 唤醒才能执行。
- 通过 sync.NewCond 函数生成一个 *sync.Cond,用于阻塞和唤醒协程
- 然后启动 10 个协程模拟 10 个人,准备就位后调用 cond.Wait() 方法阻塞当前协程等待发令枪响,这里需要注意的是调用 cond.Wait() 方法时要加锁
- time.Sleep 用于等待所有人都进入 wait 阻塞状态,这样裁判才能调用 cond.Broadcast() 发号施令
- 裁判准备完毕后,就可以调用 cond.Broadcast() 通知所有人开始跑了
sync.Cond 有三个方法,它们分别是:
1.Wait,阻塞当前协程,直到被其他协程调用 Broadcast 或者 Signal 方法唤醒,使用时需要加锁,使用 sync.Cond 中的锁即可,也就是 L 字段。
2.Signal,唤醒一个等待时间最长的协程。
3.Broadcast,唤醒所有等待的协程。
注意:在调用 Signal 或者 Broadcast 之前,要确保目标协程处于 Wait 阻塞状态,不然会出现死锁问题。
func race(){
cond :=sync.NewCond(&sync.Mutex{})
var wg sync.WaitGroup
wg.Add(11)
for i:=0;i<10; i++ {
go func(num int) {
defer wg.Done()
fmt.Println(num,"号已经就位")
cond.L.Lock()
cond.Wait()//等待发令枪响
fmt.Println(num,"号开始跑……")
cond.L.Unlock()
}(i)
}
//等待所有goroutine都进入wait状态
time.Sleep(2*time.Second)
go func() {
defer wg.Done()
fmt.Println("裁判已经就位,准备发令枪")
fmt.Println("比赛开始,大家准备跑")
cond.Broadcast()//发令枪响
}()
//防止函数提前返回退出
wg.Wait()
}