协程
原先使用的进程线程,在进行切换的时候都要切换到内核态:在进程控制块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")