goroutine底层实现原理!!!!!!!!!!!!!
概念
Goroutine可以理解为一种Go语言的协程(轻量级线程),是Go支持高并发的基础,属于用户态的线程,由Goruntime管理而不是操作系统。
底层数据结构存储了非常多的上下文:
底层数据结构:
最终是一个runtime.g对象放入了队列
状态流转:
状态轮转图
1.创建
通过go关键字调用底层函数runtime.newproc()创建一个goroutine当调用该函数之后goroutine会被设置成runnable状态
func main( ) {
go func() {
fmt.Println("func routine"")
}()
fmt.Println("main goroutine")
}
创建好的这个goroutine会新建一个自己的栈空间,同时在G的sched中维护栈地址与程序计数器这些信息。每个G在被创建之后,都会被优先放入到本地队列中,如果本地队列已经满了,就会被放入到全局队列中。
2.运行
goroutine本身只是一个数据结构,真正让goroutine运行起来的是调度器。Go实现了一个用户态的调度器(GMP模型),这个调度器充分利用现代计算机的多核特性,同时让多个goroutine运行,同时goroutine设计的很轻量级,调度和上下文切换的代价都比较小。
3.调度时机
调度时机
新起一个协程和协程执行完毕
会阻塞的系统调用,比如文件io、网络io. channel、mutex等阻塞操作
time.sleep
垃圾回收之后主动调用runtime.Gosched()·运行过久或系统调用过久等等
先本地g执行,执行完全局拿(每次全局拿记得上锁,防止被多次拿),全局拿玩就去其他本地队列偷,每次偷一半(下取整),如果没得偷的了就自旋,每次最多有设定好的个数自旋等待新的任务,防止cpu浪费资源
4.阻塞
channel的读写操作、等待锁、等待网络数据、系统调用等都有可能发生阻塞,会调用底层函
数runtime.gopark(),会让出CPU时间片,让调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。
当调用该函数之后,goroutine会被设置成waiting状态
5.唤醒
处于waiting状态的goroutine,在调用runtime.goready()函数之后会被唤醒,唤醒的goroutine会被重新放到M对应的上下文P对应的runqueue中,等待被调度。
当调用该函数之后,goroutine会被设置成runnable状态
6.退出
当goroutine执行完成后,会调用底层函数runtime.Goexit()。当调用该函数之后,goroutine会被设置成dead 状态
goroutine和线程的区别
goroutine泄露场景
泄漏原因
1.Goroutine内进行channel/mutex等读写操作被一直阻塞。
2.Goroutine内的业务逻辑进入死循环,资源一直无法释放。
3.Goroutine内的业务逻辑进入长时间等待,有不断新增的Goroutine进入等待
泄露场景
1.如果输出的goroutines数量是在不断增加的,就说明存在泄漏
2.nil channel,对空channel读写
3.channel如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。
1.nil channel
func main() {
fmt.Println("before goroutines: ",runtime.NumGoroutine( ) )
block1()
time.Sleep(time.Second * 1)
fmt.Println("after goroutines: ", runtime.NumGoroutine()
}
func block1() {
var ch chan int
for i := 0; i < 10; i++ {
go func() {
<-ch3}()
}
}
2.接受不发送
channel发送数量草果channel接收数量,造成了阻塞
func block2() {
ch := make( chan int)
for i := 0; i < 10; i++ {
go func() {
ch <- 1}()
}
}
3.只接受不发送
channel接收数量超过了channelf发送的数量,也会造成阻塞
func block3( ) {
ch := make(chan int)
for i := 0; i < 10; i++ {
go func() {
<-ch}()
}
}
4.http request body未关闭
resp.Body.Close(),为被调用,groutine不会退出
5.互斥锁忘记解锁
第一个协程获取sync.Mutex加锁了,但是他可能在处理业务逻辑,又或是忘记Unlock 了。
因此导致后面的协程想加锁,却因锁未释放被阻塞了
func block5() {
var mutex sync.Mutex
for i := 0; i < 10; i++ {
go func() {
mutex.Lpck()}()
}
}
6.sync.WaitGroup原语使用不当
由于wg.Add的数量与wg.Done 数量并不匹配,因此在调用wg.wait方法后一直阻塞等待
func block6() {
var wg sync.waitGroup
for i := 0; i < 10; i++ {
go func() {
wg.Add(2)
wg.Done()
wg.wait()}()
}
}
如何排查
1.单个函数︰调用runtime.NumGoroutine方法来打印执行代码前后Goroutine的运行数量,进行前后比较,就能知道有没有泄露了。
2.生产/测试环境:使用PProf 实时监测Goroutine的数量
如何查看正在执行的goroutine的数量?
因为go服务一般是在线服务,所以我们引入pprof package
//在程序中引入pprof package
import _ "net/http/pprof"
如何控制并发的goroutine数量
为什么要控制goroutine并发的数量?
在开发过程中,如果不对goroutine加以控制而进行滥用的话,可能会导致服务整体崩溃。比如耗尽系统资源导致程序崩溃,或者CPU使用率过高导致系统忙不过来。
用什么方法控制goroutine并发的数量?
有缓冲channel利用缓冲满时发送阻塞的特性
有数据才创建!!!!!!!!!!!!这里最多支持三个goroutine