go笔记之——goroutine

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值