Golang协程
Do not communicate by sharing memory; instead, share memory by communicating.
通信共享内存—channel
channel,管道,可以理解为队列,用于goroutine之间传递指定类型的值来同步运行和通讯,通道有几种类型:
分类方式 | 种类 |
---|---|
通信方向 | 双向,仅接收,仅发送 |
有无缓冲(buffer) | 无缓冲,有缓冲 |
- 按照通信方向区分:
chan time.Time // 双向,可用来发送和接收time.Time类型的值
chan<- float64 // 仅发送,仅可用来发送float64类型的值(向channel发送)
<-chan int // 仅接收,仅可用来接收int类型的值(从channel接收)
- 按照有无缓冲区分:
nbc := make(chan int) // 不带缓冲的int类型管道
bc := make(chan int, 10) // 带缓冲的int类型管道
// 获取channel容量
n := cap(bc) // n = 10
// 获取channel中的元素数量
l := len(bc) // l = 0
管道的使用
- 数据的发送与接收
从缓存channel中取数据是无序的
nbc <- 3 // 往管道nbc发送3
i := <-bc // 从管道bc接收一个值,根据上例,类型为int
// 若channel 不带buffer,或容量已满,向其发送数据时,发送方会阻塞
// 若channel 为空,从其接收数据时,接收方或阻塞
- 关闭管道
close(ch),channel关闭意味着数据的写入(或生产)已经完成,此时不能再向该channel发送数据,否则会panic,但可以从中读取数据,即使channel已空,读取也不会panic,只是返回其数据类型的0值
注意:关闭已关闭的或 len == 0 的channel时, 会panic;关闭只读通道无法编译成功
ch := make(chan string)
go func() {
ch <- "Hello!"
close(ch)
}()
v, ok := <-ch // 变量v的值为空字符串"",变量ok的值为false
fmt.Println(v, ok) // 输出Hello! true
fmt.Println(<-ch) // 输出零值 - 空字符串"",不会阻塞
v, ok = <-ch // 变量v的值为空字符串"",变量ok的值为false
fmt.Println(v, ok) // 输出"" false
循环从channel中接收数据, for range chan 会在channel 关闭后自动退出循环:
ch := make(chan string)
// 不关心收到的值的情况
ticker := time.NewTicker(time.Second)
for range ticker.C{
fmt.Println("1 second pass")
}
// 需要收到的值的情况
for v := range ch{
fmt.Println(v)
}
// 等同于
for {
if v, ok := <-ch;ok{
fmt.Println(v)
}else{
}
- 无缓冲和有缓冲channel的应用
无缓冲的channel 常用于信号量的发送(https://play.golang.org/p/GpzSJqZQbHr)
var ch = make(chan struct{}) // 使用struct{},表示不在乎信息内容,且struct{}占用内存为0
// 假设Add是一个很费时的io操作
func Add(a, b int) {
time.Sleep(5 * time.Second)
fmt.Printf("%d+%d=%d\n", a, b, a+b)
ch <- struct{}{}
}
func main() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
go Add(i, j)
}
}
// 100加法个全部计算后退出
count := 0
for {
if _, ok := <-ch; ok {
count++
}
if count == 100 {
fmt.Println("all job done!")
break
}
}
}
有缓冲的channel 用于生产者消费者模型(cap = 1)或数据临时存放(cap > 1)
生产消费模型的channel buffer 最好设为1,即生产和消费的速度相当或消费快于生产,否则即使调高buffer,也会产生任务积压
// 生产者消费者模型
type Adder struct{
Num [2]int
Sum int
}
var bf = make(chan Adder,1)
var r = rand.New(rand.NewSource(time.Now().Unix()))
// 生产者
func produce(){
num := [2]int{r.Intn(100),r.Intn(100)}
bf <- Adder{
Num: num,
}
}
//消费者
func consume(ad Adder) Adder{
ad.Sum = ad.Num[0]+ad.Num[1]
return ad
}
func main(){
go func(){
for{
time.Sleep(time.Second)
produce()
}
}()
for v := range bf{
go func(t Adder){
temp:= consume(t)
fmt.Println(temp)
}(v)
}
}
数据临时存放(类似slice,只是无需锁操作,效率更高)
// 场景:获取一月的平均气温,但接口只支持日查询
// 此处需用到sync.WaitGroup,是Go语言标准库的一部分,用于等待一组goroutine结束运行
type Data struct{
Date time.Time
Temp float64
}
func main(){
start := time.Date(2020,5,1,0,0,0,0,time.Local)
end := time.Date(2020,5,31,0,0,0,0,time.Local)
wg := sync.WaitGroup{}
ch := make(chan Data, 31)
for ;start.Before(end);start = start.AddDate(0,0,1){
wg.Add(1)
go func(t time.Time){
defer wg.Done()
data := fetchAPI(t)
ch<- data
}(start)
}
// 等待5月所有日期查询完毕
wg.Wait()
var avgTemp float64
for v := range ch{
aveTemp += v.Temp
}
avgTemp /=31
fmt.Println("avg temp of May = ", avgTemp)
}
func fetchAPI(date time.Time) Data{
data := get("url", date)
return data
}
协程—goroutine
Golang中使用go 关键字,即可开启一个goroutine,常用的有两种方式:
go Add(1,2)
// go func(形参){函数体}(实参),必须有实参部分
go func(a,b int){
fmt.Printf("%d+%d=%d\n", a, b, a+b)
}(1,2)
在循环中使用匿名函数的方式创建goroutine,使用不当会出现很多问题,例如:
func main(){
for i:= 0; i< 5; i++{
go func(){
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
// Output:
// go vet 会有如下报警:
// ./prog.go:16:16: loop variable i captured by func literal
5
5
5
5
5
// 循环中创建协程后,当匿名函数执行时,i的值已累加至5,故输出的值都为5,正确写法如下
func main(){
for i:= 0; i< 5; i++{
go func(t int){
fmt.Println(t)
}(i)
}
time.Sleep(time.Second)
}
// 或
func main(){
for i:= 0; i< 5; i++{
t := i
go func(){
fmt.Println(t)
}()
}
time.Sleep(time.Second)
}
// 延伸:闭包
recover
在Golang中,任意一个goroutine 中发生panic并且未被recover捕获,整个程序都会停止运行,故需在可能panic的goroutine中按需recover panic
// 无recover(https://play.golang.org/p/V1vgZh4hCCK)
func main() {
// 协程1
go func() {
for {
fmt.Println("I am doing something here.")
time.Sleep(time.Second)
}
}()
// 协程2
go func() {
for i := 10; i > (-10); i-- {
fmt.Println(5/i)
}
}()
time.Sleep(10 * time.Second)
}
// 当i循环至0时,5/i panic,主协程退出,程序运行
// 使用recover 捕获panic(https://play.golang.org/p/3TLy6PPCnxP)
func main() {
go func() {
for {
fmt.Println("I am doing something here.")
time.Sleep(time.Second)
}
}()
go func() {
defer func() {
if err := recover();err != nil{
log.Println(err)
}
}()
for i := 10; i > (-10); i-- {
fmt.Println(5/i)
}
}()
time.Sleep(10 * time.Second)
}
lock
Golang中channel的所有操作都是协程安全的,不需要加锁。但也提供了锁工具,类似Java的synchronized(Golang中的map, slice等都不是协程安全的)
//以slice为例
func main() {
raceSlice()
}
func raceSlice(){
x := make([]int, 0)
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for i:=0;i<100;i++{
x = append(x, i)
}
}()
go func() {
defer wg.Done()
for i:=0;i<100;i++{
x = append(x, i)
}
}()
wg.Wait()
fmt.Println(len(x))
}
// 预期结果为200,但实际结果为<=200的随机数,不加锁写slice时会导致数据丢失
// map与此类似,并发读写、并发写都会导致panic
func raceMap(){
m := make(map[int]int)
go func() { //开一个协程写map
for i := 0; i < 100; i++ {
m[i] = i
}
}()
go func() { //开另一个协程读map
for i := 0; i < 100; i++ {
m[i] = i
}
}()
time.Sleep(time.Second * 20)
}
并发读写时产生数据竞争(竞态 race)
go build, go run, go test 命令都可添加 —race 参数,检测竞态
lock的分类和用法
lock分为互斥锁(sync.Mutex)和读写锁(sync.RWMutex)
Mutex, 互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景
注意点:
- Unlock一个未加锁的锁,会导致运行时错误
- Lock一个已加锁的锁,会导致死锁
// 上面例子的并发写slice,加锁
func raceSlice(){
x := make([]int, 0)
wg := sync.WaitGroup{}
lock := sync.Mutex{} //lock在使用后不能复制,故在需要传递锁的情景下需传递指针
wg.Add(2)
go func() {
defer wg.Done()
for i:=0;i<100;i++{
lock.Lock()
x = append(x, i)
lock.Unlock()
}
}()
go func() {
defer wg.Done()
for i:=0;i<100;i++{
lock.Lock()
x = append(x, i)
lock.Unlock()
}
}()
wg.Wait()
fmt.Println(len(x))
}
// Output: 200
sync.RWMutex 读写锁的规则:
- 锁写时,只有获得写锁的goroutine可以写,其他不能读也不能写
- 锁读时,其他goroutine可以读,但不能写
// 规则2 没有锁写时可以多个goroutine同时读
var m *sync.RWMutex = &sync.RWMutex{}
func main() {
// 多个同时读
go read(1)
go read(2)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"start reading")
m.RLock()
println(i,"reading")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"read over")
}
// Output
// 1 start reading
// 1 reading
// 2 start reading (1没读完时,2已经开始读)
// 2 reading
// 1 read over
// 2 read over
var m *sync.RWMutex = &sync.RWMutex{}
func main() {
// 写的时候不能
go write(1)
go read(2)
go write(3)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"start reading")
m.RLock()
println(i,"reading")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"read over")
}
func write(i int) {
println(i,"start writing")
m.Lock()
println(i,"writing")
time.Sleep(1*time.Second)
m.Unlock()
println(i,"write over")
}
//Output:
// 1 start writing
// 1 writing
// 2 start reading(等待写锁解锁)
// 3 start writing
// 1 write over(写锁解锁)
// 2 reading
// 2 read over(读锁解锁)
// 3 writing (等待读锁解锁)
lock 也可以匿名嵌入到其他结构体中使用
type SomeReader struct{
Source string
*sync.Mutex
}
func(s SomeReader) Read(){
s.Lock()
...
s.Unlock()
}
sync/atomic
atomic 包提供原子操作,如数字的载入、增减、比较、交换、存储等,无序加锁,操作更简单。主要用途在于在协程中对数字进行操作。
func main() {
var n int32
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
n++ // 多个协程同时操作n,产生竞态
wg.Done()
}()
}
wg.Wait()
fmt.Println(n)
}
// Output: 966(<=1000的数)
func main(){
var n int32
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
atomic.AddInt32(&n, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println(n) // 在协程中读取时,使用atomic.LoadInt32(&n)
}
// Output: 1000
sync
sync包提供编写并发安全代码的工具,包括同步锁,读写锁,WaitGroup,条件变量Cond,Once,协程安全的Map, 对象池Pool等
Cond
func main() {
lock := &sync.Mutex{}
cond := sync.NewCond(lock)
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cond.L.Lock() // 使用cond前必须加锁
defer cond.L.Unlock()
cond.Wait()
fmt.Println("条件满足")
}()
}
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
if i == 4 {
cond.L.Lock()
//defer cond.L.Unlock() // 循环中不要使用defer
//cond.Signal() // 唤醒一个等待中的goroutine,此例子中,Signal 会唤醒5个等待协程中的1个,其余4个无法唤醒,死锁
cond.Broadcast() // 唤醒所有等待中的goroutine
cond.L.Unlock()
}
}
}()
wg.Wait()
}
Once
once可以保证函数在多个goroutine中只执行一次
func main() {
wg := sync.WaitGroup{}
once := sync.Once{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(t int) {
defer wg.Done()
once.Do(func() {
fmt.Println("run once")
})
fmt.Println(t)
}(i)
}
wg.Wait()
}
Pool实现对象池,在需要频繁创建销毁对象的地方使用对象池,可以降低GC的压力,提高程序性能
Map实现了协程安全的哈希表
select
select用于从一组可能的通讯中选择一个进一步处理。如果任意一个通讯都可以进一步处理,则从中随机选择一个,执行对应的语句。否则,如果又没有默认分支(default case),select语句则会阻塞,直到其中一个通讯完成。
func main() {
ch := make(chan int, 1)
s := make(chan struct{})
go write(ch)
go func() {
timer := time.NewTimer(time.Second * 5)
select {
case val:= <-ch: // 在5s内从ch收到了值
fmt.Println("receive before timeout, value is ", val)
s<- struct{}{}
case <-timer.C: // 在5s内未从ch收到值
fmt.Println("timeout")
s<- struct{}{}
}
}()
<-s
}
func write(c chan<- int){
r := rand.New(rand.NewSource(time.Now().Unix()))
time.Sleep(time.Duration(r.Intn(10)) * time.Second)
c<- 1
}
select 可以进行嵌套,以处理同时收到数据时代码执行的优先级问题
下面代码,若highChan和lowChan 同时收到数据,第一段代码会随机处理,第二段代码必会先处理高优先级(handleHigh)的数据。
// 1. 无优先级
for {
select {
case data := <- highChan:
handleHigh(data)
case data := <- lowChan:
handleLow(data)
}
}
// 2. 无优先级
for {
select {
case data := <- highChan:
handleHigh(data)
default:
select {
case data := <- highChan:
handleHigh(data)
case data := <- lowChan:
handleLow(data)
}
}
}