这里写自定义目录标题
并发
协程
协程(coroutine)是一种轻量级的线程,或者说是用户态的线程,不受操作系统直接调度,由Go语言自身的调度器进行运行时调度,因此上下文切换开销非常小,这也是为什么Go的并发性能很不错的原因之一。
package main
import "fmt"
func main() {
//Go中,创建一个协程十分的简单,仅需要一个go关键字,就能够快速开启一个协程,go关键字后面必须是一个函数调用
go fmt.Println(123)
}
这个例子执行过后在大部分情况下什么都不会输出,协程是并发执行的,系统创建协程需要时间,而在此之前,主协程早已运行结束,一旦主线程退出,其他子协程也就自然退出了。并且协程的执行顺序也是不确定的,无法预判的
解决
最简单的做法就是让主协程等一会儿,需要使用到time
包下的Sleep
函数,可以使当前协程暂停一段时间
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go fmt.Println(i)
time.Sleep(time.Millisecond)
}
fmt.Println("end")
}
对于并发的程序而言,不可控的因素非常多,执行的时机,先后顺序,执行过程的耗时等等,倘若循环中子协程的工作不只是一个简单的输出数字,而是一个非常巨大复杂的任务,耗时的不确定的,那么依旧会重现之前的问题。
import (
"fmt"
"math/rand"
"time"
)
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go hello(i)
time.Sleep(time.Millisecond)
}
time.Sleep(time.Millisecond)
fmt.Println("end")
}
func hello(i int) {
// 模拟随机耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))//当耗时超过1毫秒 依旧出现该问题
fmt.Println(i)
}
因此time.Sleep
并不是一种良好的解决办法,幸运的是Go提供了非常多的并发控制手段,常用的并发控制方法有三种:
三种方法有着不同的适用情况,WaitGroup
可以动态的控制一组指定数量的协程,Context
更适合子孙协程嵌套层级更深的情况,管道更适合协程间通信。对于较为传统的锁控制,Go也对此提供了支持:
管道
channel
,译为管道,Go对于管道的作用如下解释:
即通过消息来进行内存共享,channel
就是为此而生,它是一种在协程间通信的解决方案,同时也可以用于并发控制,先来认识下channel
的基本语法。
var ch chan int
//管道的声明语句 此时管道还未初始化 值为nil
fmt.Println(ch)//nil
创建
在创建管道时,有且只有一种方法,那就是使用内置函数make
make
函数接收两个参数,第一个是管道的类型,第二个是可选参数为管道的缓冲大小
make(chan int,1)//缓冲区大小为1的管道
在使用完一个管道后一定要记得关闭该管道,使用内置函数close
来关闭一个管道,该函数签名如下
func close(c chan<- Type)
package main
func main() {
intCh := make(chan int, 1)
close(intCh)
}
有些时候使用defer
来关闭管道可能会更好。
package main
func main() {
intCh := make(chan int, 1)
defer close(intCh)
}
读写
写入 ch <-
读取 <-ch
<-
很生动的表示了数据的流动方向,来看一个对int
类型的管道读写的例子
import "fmt"
func main() {
intCh := make(chan int, 1)
defer close(intCh)
intCh <- 123
//<-intCh接受两个返回值 ok 是否读取成功 true'成功 false失败
ints, ok := <-intCh
if ok {
fmt.Println(ints)
} else {
fmt.Println("error")
}
}
管道中的数据流动方式与队列一样,即先进先出(FIFO),协程对于管道的操作是同步的,在某一个时刻,只有一个协程能够对其写入数据,同时也只有一个协程能够读取管道中的数据。
无缓冲
package main
import "fmt"
func main() {
ch1 := make(chan int)
fmt.Println(ch1)
}
对于无缓冲管道而言,因为缓冲区容量为0,所以不会临时存放任何数据。
因为无缓冲管道无法存放数据,在向管道写入数据时必须立刻有其他协程来读取数据,否则就会阻塞等待,读取数据时也是同理,这也解释了为什么下面看起来很正常的代码会发生死锁。
package main
import "fmt"
func main() {
ch1 := make(chan int)
defer close(ch1)
ch1 <- 123
n := <-ch1
fmt.Println(n)
}
//deadlock 死锁
无缓冲管道不应该同步的使用,正确来说应该开启一个新的协程来发送数据
package main
import "fmt"
func main() {
ch1 := make(chan int)
defer close(ch1)
go func() {
ch1 <- 123
}()
n := <-ch1
fmt.Println(n)
}
有缓冲
当管道有了缓冲区,就像是一个阻塞队列一样,读取空的管道和写入已满的管道都会造成阻塞。
package main
import "fmt"
func main() {
ch01 := make(chan int, 1)
defer close(ch01)
//
ch01 <- 123
//写入已满的管道
ch01 <- 222
fmt.Println(112)
}
package main
import "fmt"
func main() {
ch01 := make(chan int, 1)
defer close(ch01)
//读取空的管道
x := <-ch01
fmt.Println(x)
}
都会造成阻塞,dead lock
//尽管可以顺利运行,但这种同步读写的方式是非常危险的,一旦管道缓冲区空了或者满了,将会永远阻塞下去,因为没有其他协程来向管道中写入或读取数据。
package main
import "fmt"
func main() {
ch01 := make(chan int, 1)
defer close(ch01)
ch01 <- 123
x := <-ch01
fmt.Println(x)
}
来看看下面的一个例子
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
ch1 := make(chan struct{})
ch2 := make(chan struct{})
defer func() {
close(ch)
close(ch1)
close(ch2)
}()
//负责写入
go func() {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("写入", i)
}
ch1 <- struct{}{}
}()
//负责读取
go func() {
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond)
fmt.Println("读取", <-ch)
}
ch2 <- struct{}{}
}()
fmt.Println("写入完毕", <-ch1)
fmt.Println("读取完毕", <-ch2)
}
两个fmt遇到管道读取阻塞,需要等到ch1和ch2写入后才能读取,再看回来就是两个协程里面,两个循环,for里面遇到了管道,但是管道ch的缓冲区是3,循环是10次,那么直接3次写满,协程又阻塞,这时候等到第二个go开始读取一个出来,第一个go又往里面写入一个,等到都写入读取完,开始写入ch1和ch2,最后读取ch1和ch2
利用管道的阻塞条件,可以很轻易的写出一个主协程等待子协程执行完毕的例子
func main() {
// 创建一个无缓冲管道
ch := make(chan struct{})
defer close(ch)
go func() {
fmt.Println(2)
// 写入
ch <- struct{}{}
}()
// 阻塞等待读取
<-ch
fmt.Println(1)
}
//2
//1
通过有缓冲管道还可以实现一个简单的互斥锁,看下面的例子
var count = 0
// 缓冲区大小为1的管道
var lock = make(chan struct{}, 1)
func Add() {
// 加锁
lock <- struct{}{}
fmt.Println("当前计数为", count, "执行加法")
count += 1
// 解锁
<-lock
}
func Sub() {
// 加锁
lock <- struct{}{}
fmt.Println("当前计数为", count, "执行减法")
count -= 1
// 解锁
<-lock
}
通过内置函数读取管道缓冲区中数据的个数和访问管道缓冲区大小
func main() {
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(len(ch), cap(ch))
//3 5
}
单向通道
关闭通道的内置函数close
的函数签名就用到了单向通道。
close
函数的形参是一个只写通道,After
函数的返回值是一个只读通道,所以单向通道的语法如下:
//只写通道 箭头符号<-在后,就是只写通道,
chan<-type
//只读通道 箭头符号<-在前,就是只读通道
<-chan type
当尝试对只读的管道写入数据时,将会无法通过编译
func main() {
timeCh := time.After(time.Second)
//不能写入 无效运算: timeCh <- time.Now() (发送到仅接收类型 <-chan Time)
timeCh <- time.Now()
}
双向管道可以转换为单向管道,反过来则不可以。通常情况下,将双向管道传给某个协程或函数并且不希望它读取/发送数据,就可以用到单向管道来限制另一方的行为。
package main
import "fmt"
func main() {
i := make(chan int, 1)
go write(i)
fmt.Println(<-i)
}
//接受一个单向的只写通道
func write(ch chan<- int) {
ch <- 1
//无法读取
fmt.Println(<-ch)//无效运算: <-ch (从仅发送类型 chan<- int 接收)
}
chan
是引用类型,即便Go的函数参数是值传递,但其引用依旧是同一个,这一点会在后续的管道原理中说明。
package main
import "fmt"
func main() {
i := make(chan int, 1)
defer close(i)
go changeSlice(i)
fmt.Println(<-i)//读取是123
}
func changeSlice(a chan int) {
a <- 123
}
注意点
下面是一些总结,以下几种情况使用不当会导致管道阻塞:
1.读写无缓冲管道
func main() {
// 创建了一个无缓冲管道
intCh := make(chan int)
defer close(intCh)
// 发送数据
intCh <- 1
// 读取数据
ints, ok := <-intCh
fmt.Println(ints, ok)
}
2.读取空缓冲区的管道
func main() {
// 创建的有缓冲管道
intCh := make(chan int, 1)
defer close(intCh)
// 缓冲区为空,阻塞等待其他协程写入数据
ints, ok := <-intCh
fmt.Println(ints, ok)
}
3.写入满缓冲区的管道
func main() {
// 创建的有缓冲管道
intCh := make(chan int, 1)
defer close(intCh)
intCh <- 1
// 满了,阻塞等待其他协程来读取数据
intCh <- 1
}
4.管道为nil时,做读写操作
func main() {
var intCh chan int
// 写
intCh <- 1
}
func main() {
var intCh chan int
// 读
fmt.Println(<-intCh)
}
5.导致panic
5.1关闭一个nil
管道
当管道为nil
时,使用close
函数对其进行关闭操作会导致panic`
func main() {
var intCh chan int
close(intCh)
}
5.2写入已关闭的管道
func main() {
intCh := make(chan int, 1)
close(intCh)
intCh <- 1
}
5.3关闭已关闭的管道
func main() {
ch := make(chan int, 1)
defer close(ch)
go write(ch)
fmt.Println(<-ch)
}
func write(ch chan<- int) {
// 只能对管道发送数据
ch <- 1
close(ch)
}
for range
package main
import "fmt"
func main() {
i := make(chan int, 10)
defer close(i)
go func() {
for x := 0; x < 10; x++ {
i <- x
}
}()
for n := range i {
fmt.Println(n)
}
}
forrange遍历管道只有一个返回值就是管道的元素值
for range
会遍历读取管道缓冲区中的元素,当管道缓冲区为空时,就会阻塞等待,直到有其他协程向管道中写入数据才会继续读取数据,输出会如下:
0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!
需要改进以上代码:
在管道写入完毕后将其关闭
package main
import "fmt"
func main() {
i := make(chan int, 10)
go func() {
for x := 0; x < 10; x++ {
i <- x
}
//不会在发生死锁
defer close(i)
}()
for n := range i {
fmt.Println(n)
}
}
即便管道已经关闭,对于有缓冲管道而言,依旧可以读取数据,并且第二个返回值仍然为true
package main
import "fmt"
func main() {
ch := make(chan int, 10)
for i := 0; i < 5; i++ {
ch <- i
}
// 关闭管道
close(ch)
// 再读取数据
for i := 0; i < 7; i++ {
n, ok := <-ch
fmt.Println(n, ok)
}
}
0 true
1 true
2 true
3 true
4 true
0 false//第六次 已经读取完了管道内的数据 false代表读取失败
0 false//第七次
由于**管道已经关闭了,即便缓冲区为空,再读取数据也不会导致当前协程阻塞,**可以看到在第六次遍历的时候读取的是零值,并且ok
为false
。
注意:
关于管道关闭的时机,应该尽量在向管道发送数据的那一方关闭管道,而不要在接收方关闭管道,因为大多数情况下接收方只知道接收数据,并不知道该在什么时候关闭管道。
select
与switch类似
存在default时,case都读取失败后,最后会default
每一个case
只能操作一个管道,且只能进行一种操作,要么读要么写,当有多个case
可用时,select
会伪随机的选择一个case
来执行
如果所有case
都不可用,就会执行default
分支,倘若没有default
分支,将会阻塞等待,直到至少有一个case
可用。
package main
import "fmt"
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
go send(chA, 1)
go send(chB, 2)
go send(chC, 3)
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
default:
fmt.Println("所有管道都不可用")
}
}
func send(ch chan int, i int) {
ch <- i
}
取消default 至少等待一个管道写入后开始读取 1 true
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
go send(chA, 1)
//go send(chB, 2)
//go send(chC, 3)
select {
case n, ok := <-chA:
fmt.Println(n, ok)// 1 true
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
//default:
// fmt.Println("所有管道都不可用")
}
}
func send(ch chan int, i int) {
ch <- i
}
加上for 死循环可实现一直监听 chan但是会造成死锁
package main
import "fmt"
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
go send(chA, 1)
go send(chB, 2)
go send(chC, 3)
for {
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
//default:
// fmt.Println("所有管道都不可用")
}
}
}
func send(ch chan int, i int) {
ch <- i
}
添加跳出for循环的条件 超时1秒钟 <-time.After(time.Second) 和使用标签进行跳出循环
<-time.After(time.Second) 是一个只读管道 1秒钟后可读取
package main
import "fmt"
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
go send(chA, 1)
go send(chB, 2)
go send(chC, 3)
Loop:
for {
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
case n,ok:=<-time.After(time.Second)
fmt.Println("超时1秒,跳出死循环")
break Loop
}
}
}
func send(ch chan int, i int) {
ch <- i
}
通过for
循环配合select
来一直监测三个管道是否可以用,并且第四个case
是一个超时管道,超时过后便会退出循环,结束子协程。
B 2 true
C 3 true
A 1 true
2023-06-02 16:13:47.8727058 +0800 CST m=+1.002683901 true
超时1秒跳出循环
超时
上一个例子用到了time.After
函数,其返回值是一个只读的管道,该函数配合select
使用可以非常简单的实现超时机制,
func main() {
chA := make(chan int)
defer close(chA)
go func() {
time.Sleep(time.Second * 2)//2秒后进行chA的写入
chA <- 1
}()
select {
case n := <-chA:
fmt.Println(n)
case <-time.After(time.Second)://超出1秒 进入该case
fmt.Println("超时")
}
}
永久阻塞
当select
语句中什么都没有时,就会永久阻塞,例如
func main() {
fmt.Println("start")
select {}
fmt.Println("end")
}
//start
//fatal error: all goroutines are asleep - deadlock! 死锁
在select
的case
中对值为nil
的管道进行操作的话,并不会导致阻塞,该case
则会被忽略,永远也不会被执行。例如下方代码无论执行多少次都只会输出timeout。
func main() {
var nilCh chan int //没有对nilCh进行初始化 为nil
select {
case <-nilCh:
fmt.Println("read")
case nilCh <- 1:
fmt.Println("write")
case <-time.After(time.Second):
fmt.Println("timeout")
}
}
waitGroup
sync.WaitGroup
是sync
包下提供的一个结构体,WaitGroup
即等待执行,使用它可以很轻易的实现等待一组协程的效果。该结构体只对外暴露三个方法。
Add
方法用于指明要等待的协程的数量
func (wg *WaitGroup) Add(delta int)
Done
方法表示当前协程已经执行完毕
func (wg *WaitGroup) Done()
Wait
方法等待子协程结束,否则就阻塞
func (wg *WaitGroup) Wait()
基础
package main
import (
"fmt"
"sync"
)
func main() {
wait := sync.WaitGroup{}
// 指定子协程的数量
wait.Add(1) //1
go func() {//每当一个协程执行完毕时调用Done,计数就-1,直到减为0
fmt.Println(1)
wait.Done() //-1
}()
wait.Wait() //等待协程结束
fmt.Println("协程结束了")
}
//
案例
package main
import (
"fmt"
"sync"
)
func main() {
wait := sync.WaitGroup{}
wait1 := sync.WaitGroup{}
wait.Add(10)
for i := 0; i < 10; i++ {
wait1.Add(1) //每次循环创建wait1 协程+1
go func() {
fmt.Println("子协程第", i, "次结束")
wait1.Done() //-1 协程执行完一次 wait1 协程-1 变成0
wait.Done() //-1 协程执行完一次 wait 协程-1
}()
//每次等待wait1 协程执行完毕
wait1.Wait()
}
//主协程 等待wait执行完毕
wait.Wait()
fmt.Println("全部协程结束完毕")
}
子协程第 0 次结束
子协程第 1 次结束
子协程第 2 次结束
子协程第 3 次结束
子协程第 4 次结束
子协程第 5 次结束
子协程第 6 次结束
子协程第 7 次结束
子协程第 8 次结束
子协程第 9 次结束
全部协程结束完毕
WaitGroup
通常适用于可动态调整协程数量的时候,例如事先知晓协程的数量,又或者在运行过程中需要动态调整
WaitGroup
的值不应该被复制,复制后的值也不应该继续使用,尤其是将其作为函数参数传递时,因该传递指针而不是值。倘若使用复制的值,计数完全无法作用到真正的WaitGroup
上,这可能会导致主协程一直阻塞等待,程序将无法正常运行
func main() {
var mainWait sync.WaitGroup
mainWait.Add(1)
hello(mainWait)
mainWait.Wait()
fmt.Println("end")
}
func hello(wait sync.WaitGroup) {
fmt.Println("hello")
wait.Done()
}
错误提示所有的协程都已经退出,但主协程依旧在等待,这就形成了死锁,因为hello
函数内部对一个形参WaitGroup
调用Done
并不会作用到原来的mainWait
上,所以应该使用指针来进行传递。
hello
fatal error: all goroutines are asleep - deadlock!
注意:
当计数变为负数,或者计数数量大于子协程数量时,将会引发panic