文章目录
- Go并发编程(一)Goroutine
- 进程&线程&协程
- Go调度模型
- goroutine GMP调度模型
- 整体调度流程
- 基本使用
Go并发编程(一)Goroutine
进程&线程&协程
进程
进程是程序和数据的一次动态执行的过程,是CPU资源的分配的基本的单位,进程的局限是进程的创建,撤销和切换的开销较大
线程
线程是进程之后发展出来的概念,也叫轻量级进程,是CPU调度的最小单位,线程需要共享同进程内的进程资源,创建,撤销和切换的代价小于进程
协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制
Go调度模型
对比Java的线程模型,在Java中,用户线程和内核线程是1:1的,所以每次线程的创建,销毁和切换,都涉及到用户态和内核态的切换。但是在Go线程模型中,用户态和内核态的比例是M:N,goroutine是Go提供的用户态线程,相对线程来说,粒度更细,而且goroutine切换和销毁创建不需要设计系统态切换。
原来Java中的线程其实是不区分用户线程还是内核线程的,因为是1:1的关系:但是CPU的视角只能看到内核线程,他不知道一个内核线程绑定了多少个用户线程
所以,go的思想就是将用户态的线程称为协程,内核态线程还是线程,将多个协程绑定到一个内核态线程
在go中使用的M:N的线程模型,M个协程绑定在N个线程上
- 1:1的模型中,协程的切换创建销毁都由CPU来执行,都需要切换到内核态,和java的线程模型无区别
- M:1的模型中,协程一旦阻塞会导致线程阻塞,影响其他协程执行
所以,为了解决M:1模型出现的阻塞导致的并发不可用问题,绑定了多个内核线程,当出现阻塞时,将其他线程转移到其他内核线程处理,这一步是通过Go实现的调度器完成
goroutine GMP调度模型
- G:Goroutine,待执行的任务
- M:操作系统线程,由操作系统调度器调度和管理
- P:调度器,运行在线程上的本地调度器,调度器的作用是把goroutine分配到操作系统线程上,调度器的数量默认是CPU核心数
- 全局队列:新建的Goroutine会优先放到调度的本地队列中,如果队列满了才则会将本地队列中一半的Goroutine放到全局队列
针对之前M:1模型的阻塞问题:
如果一个协程导致一个内核线程阻塞,即某个M阻塞,线程就会释放P,如果有空闲的线程,则由该线程执行P,若无,则重新创建一个新的线程执行P,复用线程:避免频繁的创建、销毁线程,而是对线程的复用。这也是goroutine的hand off机制。
整体调度流程
go func()调度流程:
work stealing机制:
- 当P的本地队列中没有待执行的协程时,会尝试从全局队列中偷取一半协程,如果全局队列为空,则尝试从其他调度器中偷取协程执行
Go调度本质是把大量的goroutine分配到少量线程上去执行,并利用多核并行,实现更强大的并发。
基本使用
func GoPrintName(name string) {
for i := 0;i < 5;i++{
fmt.Println(name)
time.Sleep(1 * time.Second)
}
}
func main() {
fmt.Println("--------")
// go关键字开启协程
go GoPrintName("kevin") // A协程
fmt.Println("--------")
go func() { // B协程
for i := 0; i < 10; i++ {
fmt.Println("demi")
time.Sleep(1 * time.Second)
}
}()
time.Sleep(10 * time.Second) // 以防主协程过快结束导致AB协程还没来得及执行就终止
}
参考:
调度器
Golang的协程调度器原理及GMP设计思想