前记
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
- 某个G能被执行完的情况:当一个M执行完当前的G后,它会从关联的P的本地队列中获取下一个G来执行。
- 某个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直接去全局队列,那样可能还需要等好久)。
有点奇怪的是,为什么是M偷P,为什么不把G直接放到别的M的P队列里呢?
原因是:M上已经有G的执行数据了,再换很复杂,没必要
如果偷取失败的话:那就放入全局队列。M去和他联系的P的waiting队列中等待。
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写数据