协程
大约 3 分钟
协程
协程 (Goroutine)
启动协程 (go关键字)
语言级的并发,语法非常简单,只要调用函数 (还能是立即执行函数) 前加一个 go
关键词,体现了go的"优雅哲学"
package main
import(
"fmt"
"strconv"
"time"
)
// 协程函数 (功能为持续输出)
func test(dur int) {
for i:=1; i<=dur; i++ {
fmt.Println("hello golang + " + strconv.Itoa(i))
// 阻塞1s
tiem.Sleep(time.Second)
}
}
// 主线程
func main() {
go test(20)
test(10)
}
启动多个协程
package main
import(
"fmt"
"time"
)
func main() {
for i:=1; i<=5; i++ {
go func(n int){
fmt.Println(n)
}(i)
}
time.Sleep(time.Second * 2)
}
// 最后输出乱序的 1 2 3 4 5
退出/等待协程 (sync包.WaitGroup类)
对应 sync 包里的 WaitGroup 类。需要使用三个方法:Add、Done、Wait
// 原型
// 计数器增加
// @param delta 可以是负数。向内部计数加上delta
// - 当内部计数器等于0,Wait方法阻塞等待的所有线程都会释放
// - 当内部计数器小于0,异常panic。
// @detail 调用顺序:
// - 本方法一般在创建新线程或其他等待事件前调用
// - 注意Add加正数的调用应该在Wait之前,否则Wait可能只会等待很少的线程。
func (wg *WaitGroup) Add(delta int)
// 计数器减少
// @detail 减一WaitGroup计数器的值,应在线程的最后执行
func (wg *WaitGroup) Done()
// 阻塞等到WaitGroup计数器减为0
func (wg *WaitGroup) Wait()
案例
package main
import(
"fmt"
"sync"
)
var wg sync.WaitGroup // 只定义而无需赋值
func main() {
for i:=1; i<=5; i++ {
wg.Add(1) // 或改写成在循环之前 wg.Add(5),适用于知道要启动多少个协程的情况
go func(n int){
fmt.Println(n)
wg.Done() // 或改写成在函数开始 defer wg.Done(),可以防止忘记
}(i)
}
wg.Wait() // 等待子协程结束 (计数器为0) 才通过
}
协程共享数据、锁
冲突问题
package main
import(
"fmt"
"sync"
)
var totalNum int // 共享变量
var wg sync.WaitGroup // 协程计数器
func add(){
defer wg.Done()
for i:=0; i<10000; i++{
totalNum = i+1
}
}
func sub(){
defer wg.Done()
for i:=0; i<10000; i++{
totalNum = i-1
}
}
func main() {
wg.Add(2)
go add()
go sub()
wg.wait()
fmt.Println(totalNum) // 这个输出的取值为 [-10000, +10000] 而非0。因为加减的底层是:拷贝到寄存器 -> 加法 -> 拷贝回内存
}
互斥锁 (sync包.Mutex类)
使用sync包的Mutex
package main
import(
"fmt"
"sync"
)
var totalNum int // 共享变量
var wg sync.WaitGroup // 协程计数器
var lock sync.Mutex // 互斥锁
func add(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁解锁
totalNum = i+1
lock.Unlock()
}
}
func sub(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁解锁
totalNum = i-1
lock.Unlock()
}
}
func main() {
wg.Add(2)
go add()
go sub()
wg.wait()
fmt.Println(totalNum) // 这个输出为0,成功,预期输出
}
读写锁 (sync包.RWMutex类)
读写锁性能比互斥锁高,经常用于读次数远远多于写次数的场景
读锁之间不冲突,读写同时发生可能有影响。
另外,写锁一般比读锁优先级比较高,避免写锁饥饿问题。
package main
import(
"fmt"
"sync"
)
var totalNum int // 共享变量
var wg sync.WaitGroup // 协程计数器
var lock sync.RWMutex // 读写锁
func read(){
defer wg.Done()
lock.RLock() // 加锁解锁 (读)
fmt.Println("开始读")
time.Sleep(time.Second)
fmt.Println("结束读")
lock.RUnlock()
}
func write(){
defer wg.Done()
lock.Lock() // 加锁解锁 (写)
fmt.Println("开始写")
time.Sleep(time.Second*2)
fmt.Println("结束写")
lock.Unlock()
}
func main() {
wg.Add(6)
for i:= 0; i<5; i++ {
go read()
}
go write()
wg.wait()
}
原理
主死从随
除了协程任务完成了或手动关闭后,协程会死亡外。
“主死从随”,意思是线程死亡时,他所创建的协程也会死亡(线程有其创建协程的所有权,自动生命周期)