一, 前言
go 语言类似 Java JUC 包也提供了一些列用于多线程之间进行同步的措施, 比如低级的同步措施有 锁, CAS, 原子变量操作类. 相比 Java 来说 go 提供了独特的基于通道的同步措施. 本节我们先来看看 go 中与锁相关的条件变量
二, 条件变量
在 java 中条件变量是与具体的锁想关联的, 在 go 中也是这样的.
- package main
- import (
- "fmt"
- "sync"
- "time"
- )
- var (
- counter int // 计数器
- wg sync.WaitGroup // 信号量
- lock sync.Mutex // 互斥锁
- cond = sync.NewCond(&lock) // 条件变量
- )
- func main() {
- //1. 获取锁
- cond.L.Lock()
- fmt.Println("main thread got lock")
- //2. 开启线程执行一些事情
- go doSomething()
- //3. 用与锁关联的条件变量的 wait 方法
- fmt.Println("main thread begin wait")
- cond.Wait()
- fmt.Println("main thread end wait")
- //4. 释放锁
- cond.L.Unlock()
- fmt.Println("main thread release lock")
- }
- func doSomething() {
- //2.1 激活阻塞到条件变量的 wait 方法的一个线程
- time.Sleep(time.Second * 2)
- //2.2 获取锁
- fmt.Println("sub thread begin get lock")
- //cond.L.Lock()
- fmt.Println("sub thread got lock")
- time.Sleep(5 * time.Second)
- cond.Signal()
- //2.3 释放锁
- //cond.L.Unlock()
- fmt.Println("sub thread release lock")
- }
go 中使用 sync.NewCond(&lock) 创建一个条件变量, 其中 lock 可以是互斥锁或者读写锁
主线程线程先获取了 lock 锁 (cond.L 就是 lock 变量), 然后开启了子线程, 然后调用了条件变量的 wait 方法, main 线程然后被阻塞, 并且 main 线程会释放获取的 lock 锁.
子线程则首先尝试获取 lock 锁, 如果是在 main 线程执行条件变量的 wait 前尝试获取锁, 则子线程会被阻塞, 等 main 线程执行到 wait 后, main 函数释放锁后, 子线程会获取锁成功.
子线程获取锁成功后, 会先休眠 5s 然后释放锁, 然后调用条件变量的 signal 方法, 这时候 main 线程则会重新获取到锁, 然后从 wait 返回.
需要注意的是调用条件变量的 signal 方法的线程在调用该方法前, 获取关联的 lock 锁这个并不是必须的, 读者可以注释获取和释放锁代码, 也是 OK 的.
与 Java 中类似调用条件变量的 signal 会激活一个线程, 调用 Broadcast 会激活所有阻塞到条件变量 wait 方法的线程.
另外需要注意, 一般调用线程应该使用循环检查方式调用条件变量的 wait 方法, 以避免虚假唤醒等问题.
三, 总结
go 中条件变量与 Java 中条件变量类似, 但是也有不同, 相同在于条件变量都是与锁关联的, 并且只有当线程获取到锁后才可以调用其关联的条件变量的 wait 方法, 否则会抛出异常, 另外当线程阻塞到 wait 方法后, 当前线程会释放已经获取的锁. 不同在于 Java 中只有当线程获取到锁后才可以调用其关联的条件变量的 signal 方法, 否则会抛出异常, 但是在 go 中调用线程调用 signal 前获取锁不是必须的.
来源: https://yq.aliyun.com/articles/690417