你需要了解的goroutine和GMP模型

goroutine的一些小知识

进程

1.操作系统程序的最小单位

2.进程用来占用内存空间

3.进程相当于一个个厂房,占用工厂的空间

线程

1.线程用来占用CPU时间

2.线程的调度需要由系统进行,开销较大

3.线程相当于工厂的生产线,占用工人的工时

4.线程里跑的程序就是生产流程

5.问题

  • 线程本身占用资源大
  • 线程的操作开销大
  • 线程切换开销大

协程

1.协程就是将一段程序的运行状态打包,可以在线程之间调度

2.将生产流程打包,使得流程不固定在生产线上

3.协程并不取代线程,协程也要在线程上运行

4.线程是协程的资源,协程使用线程这个资源

5.优势

  • 资源利用
  • 快速调度
  • 超高并发

细说协程

协程的本质runtime.g结构体

type g struct {
	stack       stack   // 协程堆栈地址
	sched     gobuf	//goroutine切换时,用于保存g的上下文,用于保存程序的运行现场
	atomicstatus atomic.Uint32 //协程的状态
	goid         uint64 //goroutine的id
    ...
    ...
}

在这里插入图片描述

stack:堆栈地址,包括low和high两个上下界

gobuf:目前程序的运行现场

- sp:栈地址,管理现在用哪个栈
- pc:程序计数器,程序运行到哪一行了

atomicstatus:协程状态

go描述线程的结构体runtime.m结构体

在这里插入图片描述

g0:g0协程,操作调度器

curg:current g,目前线程运行的协程g

mOS:不同操作系统线程的信息

单线程循环

在这里插入图片描述

在这里插入图片描述

  • 线程首先执行g0栈调用schedule()方法

  • 去g队列里拿到一个可以获取到的业务的g栈协程

  • schedule()方法调用了execute()

  • 然后execute()调用gogo()方法,会插入一个goexit栈帧

  • 然后根据g栈的gobuf里的sp和pc去执行业务,业务执行完毕

  • 调用goexit方法,goexit()会调用goexit1(),goexit1()会调用goexit0()

  • goexit0()里面会调用mcall去切换g栈和g0栈,然后切换到g0栈后,继续执行schedule()方法,继续下一个循环

在这里插入图片描述

  • 操作系统并不知道Goroutine的存在
  • 操作系统线程执行一个调度循环,顺序执行Goroutine
  • 调度循环非常像线程池
    • 问题1:协程顺序执行,无法并发
    • 问题2:多线程并发时,会抢夺协程队列的全局锁

在这里插入图片描述

本地队列解决了上述问题1。runtime.p结构体

在这里插入图片描述

m->M 线程

在这里插入图片描述

p的作用:

  • M与G之间的中介

  • P持有一些G,使得每次获取G的时候不用从全局找

  • 大大减少了并发冲突的情况

  • 如果在本地或者全局队列中都找不到G,去别的P中”偷“,增强了线程的利用率

新建协程
  • 随机寻找一个P
  • 将新协程放入P的runnext(插队)
  • 若P本地队列已满,则会放入全局队列

在这里插入图片描述

如果G占用时间很长,会保存现场,触发切换,如果该g还需要继续执行,那么放入本地队列里,以便后续循环执行,如果需要休眠,那么暂时不放入本地队列

在这里插入图片描述

如果本地队列有一个很长执行时间的g,那么还可能造成全局队列饥饿问题

在这里插入图片描述

解决方案是,每过段时间会从全局队列拿一个G放入本地队列,具体是61一次线程循环,从全局拿一个

切换时间
  • 主动挂起

    在这里插入图片描述

  • 系统调用完成时

在这里插入图片描述

总结
  • 如果协程顺序执行,会有饥饿问题
  • 协程执行中间,将协程挂起,执行其他协程
  • 完成系统调用时挂起,也可以主动挂起
  • 防止全局队列饥饿,本地队列随机抽取全局队列

抢占式调度

问题:

  • 永远都不主动挂起
  • 永远都不系统调用
  • 且这个G调度时间很长
runtime.morestack方法

只要有函数调用跳转,编译的时候,就会插入该方法。该方法本意是检查协程栈是否有足够的空间

在这里插入图片描述

标记抢占

1.系统监控到Goroutine运行超过10ms时

2.将g.stackguard0置为0xfffffade抢占标记

抢占

1.执行morestack()的时候判断是否被抢占

2.如果被抢占,回到schedule()

在这里插入图片描述

基于信号的抢占式调度

如果协程是一个for死循环,没有函数调用跳转,也没有主动调用gopark的操作,也没有系统调用,怎么搞?

线程信号
  • 操作系统有很多基于信号的底层通信方式
  • 比如SIGPIPE、SIGURG、SIGHUP
  • 线程可以注册对应信号的处理函数
信号的抢占式调度
  • 注册SIGURG信号的处理函数
  • GC工作时,向目标线程发送信号
  • 线程收到信号,触发调度

在这里插入图片描述

总结
  • 基于系统调用和主动挂起,协程可能都无法调度
  • 基于协作的抢占式调度:业务主动调用morestack()
  • 基于信号的抢占式调度:强制线程嗲用doSigPreempt()

问题:实战中协程过多问题?

  • 文件打开数限制
  • 内存限制
  • 调度开销大

解决方案

  • 优化业务逻辑

  • 利用channel缓冲区

    • 利用channel缓存机制
    • 启动协程前,向channel送入一个空结构体
    • 协程结束,取出一个空结构体
  • 协程池(tunny)

    • 预创建一定数量的协程

    • 将任务送入协程池队列

    • 协程池不断取出可用协程,执行任务

    • 慎用线程池

      • go语言的线程,已经相当于池化了
      • 二级池化增加系统复杂度
      • go语言的初衷是希望协程即用即毁,不要池化

      在这里插入图片描述

  • 调整系统资源

总结

1.为什么用协程

  • 协程用来精细利用线程
  • 写成可以支撑超高并发

2.协程是什么

  • 从runtime角度看,协程是一个可以被调度的g结构体
  • 从线程角度看,协程是一段程序,自带执行现场

3.G-M-P模型

  • 通过P结构体,达到了缓存部分G的目的
  • P本质上是一个G的本地队列,避免全局并发等待
  • 窃取式工作分配机制能够更加充分利用线程资源

4.协程并发

  • 如果协程顺序执行,会有饥饿问题
  • 协程执行中间,将协程挂起,执行其他协程
  • 完成系统调用时挂起,也可以主动挂起
  • 防止全局队列饥饿,本地队列随机抽取全局队列

5.抢占式调度

  • 基于系统调用和gopark主动挂起,协程可能无法调度
  • 基于写作的抢占式调度:业务主动调用morestack()
  • 基于信号的抢占式调度:强制线程调用doSigPreempt()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值