Goroutine底层原理研究报告

前记

os作业,本来是很正经的写着写着,突然语言有些放飞了,那就发出来吧!

参考到的链接如下:

        剩下的盛夏~http://t.csdnimg.cn/exCi1

         雪碧锅仔饭https://www.cnblogs.com/SpriteLee/p/16620208.html

基本概念

协程(Coroutine是一种程序组件,一个线程可以有多个协程

一个线程在操作系统中被分为两个部分:用户空间(用户态)和内核空间(内核态),如图1.

 图一

内核线程(也称作线程thread)和用户线程(也称作协程co-routine)是绑定的,内核线程单独整理硬件部分的事物,用户线程保证用户层面的业务并发效果,且此时在CPU视野里只有内核空间的thread。

图二

多核CPU绑定多个线程再通过协程调度器绑定多个协程。如上图2.

一个Goroutine的内存就占KB,不像进程是GB级,线程是MB级别。

小,灵活,可以有大量协程的存在。

GMP模型

G:协程 M:内核级线程 P:承载多个goroutine的运

 

注意:P实际是只和一个M建立连接的,但是它会有一个waiting的M队列,方便阻塞时切换,图中P的箭头应该是连了waiting队列中的M,后面会细说

数目关系

一个内核K对应一个M内核线程,一个P处理器可以调度一个协程队列(很多个G),当创建一个协程G后,它会进入一个P的任务队列,但如果所有P的队列都满了,会进入全局队列。如图,当前的多核CPU可以支持P处理器数目个协程的并行运行

M与G

  1. 某个G能被执行完的情况:当一个M执行完当前的G后,它会从关联的P的本地队列中获取下一个G来执行。
  2. 某个G不能被执行完的情况:当一个M正在执行一个G时,如果发生了上下文切换,M需要保存当前的执行状态,以便之后能够恢复这个G的执行

它会将当前的执行现场(包括寄存器中的值,如堆栈指针、程序计数器等)保存到当前正在执行的G对象上。这些信息保存在G的栈上,因为每个G都有自己的栈空间(和线程一样,但传统的线程上下文是保存在tcp里面的),用于存储函数调用的局部变量和返回地址。未执行完的G会重新进入任务队尾。

P与M

Processor唤醒Machine

当一个P需要执行一个G时,它会尝试唤醒一个M来运行这个G。

如果当前P关联的M的等待队列中没有可用的M(即没有正在等待被唤醒的M),那么调度器会创建一个新的M,前提是M的总数没有达到上限(在Go中,M的最大数量默认为10000)。

一旦P获取到M,它会将这个M绑定到自己身上,然后M开始执行P本地队列中的G。

Machine遇到阻塞

如果M在执行G的过程中遇到了阻塞操作,M会与当前绑定的P分离。

分离后,M会等待阻塞操作的完成(让一个内核级线程去等待)。在这个过程中,P可以继续唤醒其他的M(重新挑选一个线程来执行我队列里的其他协程任务)来执行其他G,从而实现并发

Machine阻塞后的处理

当这个负责等待阻塞完成的M完成了任务,它想要继续工作:

M会尝试偷取一个P来继续执行它的G(这句话的意思是,这个G还没执行完,只是可能做好了中途的一个IO,但是此时M已经没有P了,M是不能直接去执行G的,必须要间接通过P)。

这里的偷取是指从其他正在运行的M那里暂时借用一个P,以便能够快速恢复G的执行(这应该是一种间接的把G放入某一P的任务队列的手段,目的是不想让G直接去全局队列,那样可能还需要等好久)。

有点奇怪的是,为什么是MP,为什么不把G直接放到别的MP队列里呢?

原因是:M上已经有G的执行数据了,再换很复杂,没必要

如果偷取失败的话:那就放入全局队列。M去和他联系的Pwaiting队列中等待。

Go语言代码

任何函数只需加上go就能送给调度器运行。加上go就变成一个协程了。

创建并调用:

一个更为普通的例子:

补充:异步函数的概念

异步函数(Asynchronous Function)是一种编程语言特性,它允许函数在执行耗时的操作(如 I/O 操作、网络请求、数据库查询等)时不会阻塞程序的执行。异步函数通常会在执行完这些操作后通过某种机制(如回调函数、承诺(Promise)、异步等待(await)等)返回结果。

Channel(协程通信的方法)

​​​​​​​概念

Go 语言中,Channel 是一种特殊的类型,它提供了一种在 Goroutine 之间进行通信的方式Channel 可以被认为是 Goroutine 之间的管道,通过它可以发送和接收值。Channel 的操作是同步的,这意味着发送(send)和接收(receive)操作在 Channel 的另一端准备好之前会阻塞。

​​​​​​​结构体源码

总结:消息缓冲区 + 要读写消息的请求队列

​​​​​​​Channel操作

​​​​​​​向channel写数据

1.首先:检查等待接收队列(recvq)是否为空。如果recvq不为空,直接把要写的数据送给第一个在等待的协程。

补充Recvq不为空【即有协程想获取数据,但还没拿到,在等待】的原因可能有两个:

        1.无缓冲channel

        这个channel没有缓冲区【注意缓冲区这一概念是争对于被发送的数据而言的】,没有提前存这个数据的地方。这个机制就决定了:发送操作必须等到有接收者准备好接收数据才能继续,没有接收者,发送者也只能阻塞。但如果有接收者了,那发送者就直接发,不阻塞了

同样的,接收者准备好了,但是没有发送者,那也只能等待了。

       2.有缓冲channel且缓冲区没有数据

        理由差不多,没有协程发,没东西读,只能等。

2.如果,recvq是空的,没有协程等待数据,那就看缓冲区是否满,不满的话就写入缓冲区。

3.最后,如果缓冲区也是满的,那就把该发送Goroutine阻塞,放入发送队列【缓冲区的缓冲区】,等待另一个接收协程的唤醒。

所以话为啥这么说呢,当缓冲区有空位了,就直接唤醒呗,为什么是等待另一个接收协程的唤醒。

其实是正确的,缓冲区有空位当然是因为有接受goroutine拿走数据了,所以“一个Goroutine的接收操作会直接唤醒一个阻塞的发送操作”这句话是没问题的。

​​​​​​​Channel读数据

        1.首先:检查等待发送队列(sendq)是否为空。如果sendq不为空,那原因也会有两个

        (1)无缓冲区Channel

        没有缓冲区存的话,也没有及时出现的接收goroutine,那就只能阻塞后进入sendq了

        (2)有缓冲区Channel

        那就是缓冲区满了,只能进队列等着了

        2.然后:如果是有缓冲区的,那就读缓冲区的就行了,没缓冲区,就唤醒正在阻塞的rendq队头goroutine

        3.最后:sendq当然可能是空的,那就把请求读数据的giroutine放入recvq就行了。

关闭Channel

  【被强制摆烂了】关闭channel时会把recvq中的G全部唤醒,本该写⼊G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。

  除此之外, panic出现的常⻅场景还有:

  1. 关闭值为nil的channel

  2. 关闭已经被关闭的channel

  3. 向已经关闭的channel写数据

  • 48
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值