一、前言
go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁、CAS、原子变量操作类。本节我们先来看看go中的条件变量,在java中条件变量是与具体的锁相关联的,在go中也是这样的。
二、条件变量
在go中使用 var cond sync. NewCond(l Locker)可以创建一个与锁l相关的条件变量cond,这个锁l可以是Mutex 或者RWMutex类型的锁,该方法会返回Cond类型的指针被称为条件变量。条件变量与Java中类似,主要是使用通知/等待做多线程之间的同步,Cond类型主要有三个方法:
(c *Cond) Wait():等待方法,当一个goroutine调用cond的Wait()方法后,当前goroutine会被阻塞,直到有其他goroutine调用了该cond变量的Signal()或者Broadcast()方法;需要注意的是调用Wait()方法前,当前goroutine要通过cond.l.Lock() 先获取与cond关联的锁l,否则会抛出异常
fatal error:sync:unlock of unlocked mutex
,这点与java相同:
package main
import (
"sync"
)
var (
lock sync.Mutex //互斥锁
cond = sync.NewCond(&lock) //条件变量
)
func main() {
cond.Wait()
}
另外某个goroutine调用条件变量的Wait()方法是为了等待某个事件的发生,而当其他goroutine调用该条件变量的Singal()或者Broadcast()方法时候,等待的goroutine机会被唤醒,但是这时候等待的goroutine的事件未必已经发生了,所以一般goroutine调用条件变量的Wait()方法时候都是使用循环检查条件是否达到的方式:
package main
import (
"sync"
)
var (
lock sync.Mutex //互斥锁
cond = sync.NewCond(&lock) //条件变量
)
func main() {
cond.L.Lock() //1获取条件变量关联的锁
//2循环判断自己需要的事情是否发生
for !condition() {
cond.Wait()
}
//3.获取到自己需要的事件后做一些事情dosometing
//4.释放锁
cond.L.Unlock()
}
func condition() bool {
//
if 判断自己需要的事件已经发生 {
return true
} else {
return false
}
}
如上代码main内使用condition()方法判断自己需要的事件是否发生,如果没有则阻塞自己进行等待。如果其他调用该条件变量的Singal()或者Broadcast()方法时候,等待的goroutine机会被唤醒,然后循环一次再次调用condition()方法判断自己需要的事件是否达到了,如果没有则继续调用条件变量的Wait()方法阻塞自己,否则就继续向下运行。
另外需要注意的是当某个goroutine调用了条件变量的Wait()方法后,当前goroutine会先释放获取的与该条件变量相关的锁,然后在阻塞自己。
(c *Cond) Signal(),通知方法,当一个goroutine调用了条件变量c的该方法后,就会激活一个由于调用c的Wait()方法而被阻塞的goroutine。需要注意的是在调用c.Signal()方法前,不需要先获取与c关联的锁,这点与调用c.Wait()方法不同.
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var (
lock sync.Mutex //互斥锁
cond = sync.NewCond(&lock) //条件变量
flag int32 //标签
)
func main() {
cond.L.Lock() //1获取条件变量关联的锁
go func() {
//休眠5秒,模拟远程调用
time.Sleep(5 * time.Second)
//设置条件完成
atomic.CompareAndSwapInt32(&flag, 0, 1)
cond.Signal() //通知
fmt.Println("---sub goroutine rpc call done---")
}()
//2循环判断自己需要的事情是否发生
fmt.Println("---main wait rpc call over---")
for !condition() {
cond.Wait()
}
fmt.Println("---main do something---")
//3.获取到自己需要的事件后做一些事情dosometing
//4.释放锁
cond.L.Unlock()
}
func condition() bool {
if 1 == atomic.LoadInt32(&flag) {
return true
} else {
return false
}
}
如上例子main goroutine代码1先获取了锁,然后代码2开启了一个子goroutine,其内部使用sleep函数模拟了一次远程rpc调用,等调用完成后,原子性更新flag变量的值从0变为1,标识rpc已经完成,然后调用条件变量的Signal方法激活阻塞的一个goroutine。需要注意的是这里子goroutine在调用条件变量Signal方法前并没获取条件变量关联的锁。
main goroutine内代码3使用经典的循环方式等待rpc的调用完成,条件是condition() 函数,由于一开始flag的初始化值为0,所以main goroutine调用了条件变量的Wait()方法阻塞了自己。当子goroutine完成rpc调用,调用条件变量的Signal方法后,main goroutine就返回了,然后再次调用condition() 方法发现flag为true了说明自己等待的rpc已经完成了,就继续向下执行。
(c *Cond) Broadcast() 通知方法,当一个goroutine调用了条件变量c的该方法后,就会激活所有由于调用c的Wait()方法而被阻塞的goroutine。需要注意的是在调用c.Broadcast()方法前,不需要先获取与c关联的锁,这点与调用c.Wait()方法不同.
package main
import (
"fmt"
"sync"
"time"
)
var (
lock sync.Mutex //互斥锁
cond = sync.NewCond(&lock) //条件变量
wg sync.WaitGroup
)
func main() {
wg.Add(2)
//1.
go func() {
cond.L.Lock()
fmt.Println("---sub goroutine1 got lock and wait---")
cond.Wait()
fmt.Println("---sub goroutine1 wait return---")
cond.L.Unlock()
}()
//2.
go func() {
cond.L.Lock()
fmt.Println("---sub goroutine2 got lock and wait---")
cond.Wait()
fmt.Println("---sub goroutine2 wait return---")
cond.L.Unlock()
}()
//3.
time.Sleep(3 * time.Second)
//4.
cond.Signal()
//5
time.Sleep(5 * time.Second)
}
如上代码我们开启了两个goroutine分别调用条件变变量的Wait()方法,然后在main函数里面调用条件变量的Signal()方法,运行上面代码会发现只有一个goroutine会打印出sub * wait return。如果把代码4修改为cond.Broadcast()则会发现两个goroutine都会打印出return说明两个goroutine都被通知返回了。
三、总结
go中条件变量与Java中条件变量类似,但是也有不同,相同在于条件变量都是与锁关联的,并且只有当线程获取到锁后才可以调用其关联的条件变量的wait方法,否则会抛出异常,另外当线程阻塞到wait方法后,当前线程会释放已经获取的锁。不同在于Java中只有当线程获取到锁后才可以调用其关联的条件变量的signal方法,否则会抛出异常,但是在go中调用线程调用signal前获取锁不是必须的。