协程
原先使用的进程线程,在进行切换的时候都要切换到内核态:在进程控制块PCB中保存中断现场,然后再将CPU恢复到下一个进程的现场。
使用协程,可以将中断现场信息保存在用户空间,对内核来说是透明的。在切换协程的时候无需切换到内核态,大大地减少了开销。
如下图,协程和线程是M:N的关系,效率高与否具体取决于协程调度器的调度。
GMP
G:表示goroutine协程。
M:表示thread线程。
P:表示processor处理器。
当创建新的协程的时候,优先将其加入某一个processor处理器的本地队列,若当前processor处理器的本地队列都满了,则进入全局队列。processor处理器负责调度某一个协程执行,每一个processor能够让一个协程执行,实现多个协程并行的效果。其中,协程并行的数量取决于处理器的数量,可以通过GOMAXPROCS限定并行个数。
调度器设计策略
1、复用线程策略
work stealing 机制
当出现如下图的情况:processor处理器允许执行协程,但本地队列为空,且全局队列也为空的时候,这时候另一个处理器中的本地队列不为空,就会从另一个处理器中的本地队列中“偷”一个协程到当前的处理器中执行。避免某一处理器长时间处于空闲,浪费资源。
hand off 机制
当处理器Processor当前正在执行的协程受到某些因素进入阻塞状态的时候,这时候会先创建/唤醒新的线程,然后让原先的协程独占线程M1,处理器移动到新的线程M3处继续后续的操作。避免处理器因某一协程长时间处于阻塞状态而影响执行效率。
2、并行利用策略
我们可以通过GOMAXPROCS限定P的个数。
比如有4个CPU,但是我们只需要用2个,就可以限定P的个数为2,剩余的2个留给其他进程使用。
3、抢占策略
以前的co-routine,当协程A和CPU处于绑定关系的时候,如果此时有协程B在等待使用CPU资源,那么只能等协程A主动释放CPU,才会轮到协程B。
现在goroutine,会通过一定的策略,如果有新的协程申请资源的话,原来的协程最多使用10ms,如果未主动释放,新的协程会抢占CPU资源。
4、全局G队列
全局队列中加入和取出协程都是需要加锁解锁才能进行操作的。某些情况,如处理器P想执行协程,但当前本地队列为空,那么会先考虑从其他处理器的本地队列中获取,如果其他处理器的本地队列也为空,那么处理器就会从全局G队列中获取一个协程来执行。
创建goroutine
go 方法名
import (
"fmt"
)
func Goroutine1() {
go method10("first")
go method10("two")
go method10("three")
i:=0
// for{}代表死循环
for {
fmt.Println("main",i)
i++
// 为了观察让程序让出cpu,休眠一会儿再继续向下
//time.Sleep(1*time.Second)
}
}
func method10(name string) {
i:=0
// for{}代表死循环
for {
fmt.Println(name,":",i)
i++
// 为了观察让程序让出cpu,休眠一会儿再继续向下
//time.Sleep(1*time.Second)
}
}
执行效果如下:
之所以要在主程序中也添加死循环,是因为一旦主程序停止执行,其他协程都会停止:
func method10(name string) {
i:=0
// for{}代表死循环
for {
fmt.Println(name,":",i)
i++
// 为了观察让程序让出cpu,休眠一会儿再继续向下
//time.Sleep(1*time.Second)
}
}
func Goroutine2() {
go method10("first")
go method10("two")
go method10("three")
fmt.Println("main over")
}
执行输出结果:main over
匿名函数
直接使用在传入方法的地方定义方法。注意,如果想要方法执行,必须如func(参数) { } (参数) 在方法体结束后面添加添加()表明自调用,如果方法的定义有参数要传入参数。
func Goroutine3() {
go func() {
// 死循环
for{
fmt.Println("goroutine")
func() {
fmt.Println("sun")
}()
time.Sleep(1*time.Second)
}
// 在方法体后面加()表示自调用,如果方法有参数,还要输入参数
}()
for {
time.Sleep(1*time.Second)
fmt.Println("main")
}
}
注意:使用匿名函数不应该带返回值,因为协程是与主函数并行的,无法通过协程中方法体的return 值来获得其返回值。
退出当前协程:runtime.Goexit()
runtime.Goexit()
func Goroutine4() {
go func() {
// 死循环
for{
fmt.Println("start")
// ①如果在此处使用retun,则会退出当前方法,即协程停止,只会输出一次start
// return
func() {
// ②如果在此处使用return,只会退出当前方法,start和over继续输出
// return
fmt.Println("sun")
// ③真正结束当前协程的执行,只会输出一次start和sun
runtime.Goexit()
}()
time.Sleep(1*time.Second)
fmt.Println("over")
}
// 在方法体后面加()表示自调用,如果方法有参数,还要输入参数
}()
for {
time.Sleep(1*time.Second)
fmt.Println("main")
}
}
注意:只会结束当前执行的程序(协程),其他不会被影响,因此如上面的例子,后面还会一直输出main。
channel
前面提到,协程之间、或与主程序是并行的,无法通过return获取返回值,因此就出现了channel来实现它们的通信。
make(chan Type)make(chan Type,capacity)channel名 <- value<- channel名变量 := <- cahnnel名变量名,布尔值 := <- cahnnel名
对于只是定义了channel的,并没有使用make创建的,是nil型,无法进行收发数据。
func Channel1() {
// 创建channel方式一
c1:=make(chan int)
// 创建channel方式二:创建时指定容量,可以缓冲2个元素
c2:=make(chan string,2)
go func() {
// 存入数据到channel中
c1 <- 1001
c2 <- "hello"
c2 <- "world"
c2 <- "lena"
}()
// 接收数据,同时flag用来判断通道是否关闭或是否为空
b,flag:= <- c1
fmt.Println(flag,b) // true 1001
// 接收数据
x:= <- c2
// // 接收数据并丢弃:因为没有定义变量接收
<- c2
y:= <- c2
fmt.Println(x,y) // hello lena
}
如何保证每次读取的时候,channel中都已经有值了呢?即如何实现同步?
这里采用的是阻塞方式,当要读取的程序进行读取的时候,发现channel还没有数据,会阻塞,直到有程序存入数据。同理,当程序要存入数据的时候,如果发现读取的程序还未执行到读取数据的步骤,那么存入数据的程序就会阻塞,直到读取数据的程序准备好。类似于java中的socket通信。即永远能保证读取在存入之后,因此异步也能够保证先后顺序。
那么在创建管道的时候,是否有指定容量,有无缓冲有什么区别?
无缓冲channel
无缓冲即表示通道内不允许暂存数据。当某一个goroutine准备好了传送数据的时候,如果另一goroutine还未准备好接收数据,那么发送方会阻塞等待接收,因为通道不允许缓冲数据,只能在发送和接收都准备好的时候能传送数据。
代码检测:当无缓冲的时候,结果的确是发送方会阻塞到接收方接收数据,接收方会阻塞直到发送方要发送数据。
func Channel2() {
// 创建无缓冲channel
c:=make(chan int)
fmt.Println("容量",cap(c))
// 存入数据
go func() {
defer fmt.Println("go over")
for i:=0;i<3;i++ {
c <- i
fmt.Println("save",i,"len",len(c))
}
}()
// 等待:看数据存入多少
time.Sleep(1*time.Second)
// 取数据
for i:=0;i<3;i++ {
res := <- c
fmt.Println("get",res)
}
// 等待:确保协程执行完毕
time.Sleep(2*time.Second)
}
有缓冲channel
在创建channel的时候,可以指定缓冲容量。当一个goroutine要存入数据的时候,如果当前通道内的数据量<通道的容量,那么就可以直接把数据放进通道内存放着,继续下一步操作,无需goroutine取数据。但如果要存放数据的时候容量满了,则会阻塞。
通过代码检测是否如上述所说:
func Channel3() {
// 创建容量为3的channel
c := make(chan int,3)
// 存入数据
go func() {
defer fmt.Println("go over")
for i:=0;i<4;i++ {
c <- i
fmt.Println("save",i,"len",len(c))
}
}()
// 等待:确保数据存入
time.Sleep(1*time.Second)
// 取数据
for i:=0;i<4;i++ {
res := <- c
fmt.Println("get",res)
}
// 等待:确保协程执行完毕
time.Sleep(1*time.Second)
}
控制台输出:
save 0 len 1
save 1 len 2
save 2 len 3 // 容量满了,阻塞
get 0 // 获取一个数据
save 3 len 3 // 因为容量设置是3,会阻塞到容量小于3的时候才能存入
go over
get 1
get 2
get 3
关闭channel:close(channel)
管道确定不再使用,或者想要结束某一个尝试读取数据的循环,可以进行关闭。
// 关闭管道
func Channel4() {
c:=make(chan int)
go func() {
// 存入数据
for i := 0; i < 4; i++ {
c <- i
}
// 关闭channel
close(c)
}()
// 死循环:取出管道内数据
for {
// 只要channel还是打开状态,就会尝试获取数据
if data,ok := <-c;ok {
fmt.Println(data)
} else {
fmt.Println("nodata")
break
}
}
fmt.Println("over")
}
关闭channel后,不能再向该channel发送数据(会引发panic错误后导致接收立即返回零值)。
关闭channel后,可以继续从channel接收数据,例如使用有缓冲channel,关闭时channel中有数据未取,关闭后也可以继续取数据。
select
单流程下,一个go只能监控一个channel的状态,select可以完成同时监控多个channel的状态。
// 当参数类型一致的时候,可以不用都写
func fibonacii(data,quit chan int) {
x,y:=1,1
// 死循环,直到quit有信号
for{
// 同时监控data和quit的状态
select {
// 如果能向data内写入x,则会进入case内部
case data <- x:
x,y=x+y,x
// 如果能从quit读到数据,则会进入case
case <- quit:
fmt.Println("quit") // 退出
return // 退出当前循环
}
}
}
// select
func Channel5() {
data:=make(chan int)
quit:=make(chan int) // 如果要退出,向管道存放数据
// 取数据
go func() {
for i:=0;i<10;i++ {
// 从data中取数据 若无 则阻塞直到有数据
fmt.Println(<-data)
}
// 相当于一个退出标志位:当退出取数据循环,表明已经不需要再存数据了
quit <- 0
}()
// 存数据
fibonacii(data,quit)
}