为什么使用协程
- 线程的本身占用资源大
- 线程的操作的开销大
- 线程的切换开销大
基于此在go lang 中使用协程,降低资源的开销使用,能够容纳更多的资源
协程的本质
- runtime 中 协程本质数属于一个g 结构体
- stack: 堆栈地址
- go buf : 目前程序运行的现场
- atomicstatus: 协程的状态
协程是挂载在线程上的, 在runtime 中m 的结构体表示线程,同时g0 协程属于调度器启动协程
- g0 协程 作用 操作调度器
- curg: 目前线程运行的g
协程是如何在线程上运行
如下所示 对应线程属于 循环调用方式 不断的调用属于go 协程的业务方法 对应的版本是 go 0.X 版本内
多线程 抢占的时候 需要进行枷锁
的方式 适用于 Go 1.0 版本
操作系统线程执行一个调度的循环,顺序指向Goroutine, 调度循环非常像线程池,如果协程顺序执行,无法并发
问题: 1. 协程顺序执行 无法并发
问题: 2. 多线程并发时候,会导致协程队列的全局锁问题
所以 出现 G-M-P 调度方式
G-M-P调度模型
解决问题:多线程并发的时候,会抢占协程队列全局锁
在本地队列, 每个线程获取全局锁时候获取多个协程任务, 只有一个线程进行访问 不存在锁问题
P的作用:
- M与G之间的中间器(送料器): M 表示线程,G 协程任务,P表示本地队列
- P持有一些G,使得每次获取G不用从全局找
- 大大降低了并发的冲突问题
- 如果本地任务和全局任务也没有,go 有窃取任务
窃取形式的工作机制
- 新建协程:
- 随机寻找一个队列P,将新协程放入P的runnext(插队) ,
go 语言中新创建协程会立即执行
,只有P本地队列满时候,放入到全局队列中, 新创建协程属于 newproc函数
G-M-P 模型有个问题 导致协程的饥饿问题,如何解决这饥饿问题
如何实现协程并发
解决问题:协程顺序执行,无法并发
在go 语言中 支持进行挂起,执行其他的新协程任务,每次线程循环61次后从全局获取任务。
- 如何进行切换时机
- 主动挂起: 业务主动 go park () 方法
- 系统调用完成时(existsyscall())
- 函数调用的morestack()
问题: 如果线程永远都不进行 切换, go 语言有个runtime.morestack
在函数内部调用其他方法,go 语言会强行插入一个 runtime.morestack 方法
- morestack 本意检查协程栈是否有足够的空间
- 基于协作的抢占是调度,在moresstack 中 抢占任务 跳到 线程循环 开始的schedule方法
- 如果业务方法中有死循环问题(不调度morestack),使用 基于信号的抢占方式调度
线程可以注册对应的信号的处理函数, 注册SIGURG 信号 处理函数
2. GC工作时候,向目标线程发送信号
协程太多问题
- 在主协程中不断的产生协程,
too many socket 连接出问题
- 文件打开限制,调度太大
解决问题: - 优化业务的逻辑
- 协程池(
不建议使用,Go语言线程 已经相当于池化,不要在二级池化,go 语言初衷 希望协程即用即销毁,不要池化
)
// 预先创建一定数量协程, 将任务创建送入到等待的队列
不推荐使用
- 利用channel 缓存区
牺牲并发的性能
func do (i int , ch chan struct{}){
fmt.Println(i)
time.Sleep(time.Second)
<- ch
}
// 利用 channel的缓冲区,控制协程产生的数量,一旦缓冲区被阻塞了, 不会产生更多的协程
// 适合场景 大量批量的设置协程
高并发下通信方式 channel
- 无缓冲区 channel
接收必须要发送前不然,导致缓冲区阻塞
针对 无缓冲区的 管道,必须要先从管道里面获取接受数据,才能进行数据塞入,所以对于无
缓冲区的管道 使用 协程接收在发送的前面 不然导致阻塞
- 共享内存方案
在使用共享方案时候 需要不断的查询 共享变量的值, 如果使用channel 不需要 for 死循环轮询方式查询,直接从channel 中 获取数据,协程进行socket 通信的监听
避免协程竞争和数据冲的问题
更高级的抽象,降低开发难度,增加程序的可读性
模块间更容易解耦,增加扩展性和维护性
如何设计Channel
- 缓冲区, 发送的等待队列,接收的等待队列
- go 语言使用
Ring buffer 环形缓冲区, 大幅度降低GC的开销
- 使用互斥锁保护hchan 的结构体本身
- Channel 并不是无锁的, 主要塞入和取数据时候 需要枷锁,其他操作不需要枷锁
5.
Channel 发送数据结构的底层原理
- chansend1() 会调用chansend() 方法,c <- 转化为 runtime.chansend1()
- 直接发送 (发送数据前,已经G在休眠等待接收), 将数据直接Copy 到协程G中
- 休眠等待(没有G在休眠等待,没有缓存,发送进入发送队列中sendq)
Channel 接收数据
-
有等待的协程G,从G接收
接收数据前,已经有G在休眠中等待发送,并且channel 无缓存
-
有等待的G,从缓存接收
数据在接收前已经有G在休眠等待发送,并且这个Channel有缓存
-
接收缓存
接收G没有在休眠并且channel有数据
-
阻塞接收
没有G在休眠等待,并且缓存也没有数据,说白了 就是没有数据进来
-
总结
对于写非阻塞的channel 使用select 非阻塞的形式使用select
- 首先查看是否可以即时执行case
- 没有default , 将自己注册在 channel中休眠等待
- Time.channel 在倒计时塞入一个数据