Go语言核心编程(三) --协程

为什么使用协程

  1. 线程的本身占用资源大
  2. 线程的操作的开销大
  3. 线程的切换开销大
    基于此在go lang 中使用协程,降低资源的开销使用,能够容纳更多的资源

协程的本质

go语言协程的底层结构

  • runtime 中 协程本质数属于一个g 结构体
  • stack: 堆栈地址
  • go buf : 目前程序运行的现场
  • atomicstatus: 协程的状态
    协程是挂载在线程上的, 在runtime 中m 的结构体表示线程,同时g0 协程属于调度器启动协程
  1. g0 协程 作用 操作调度器
  2. curg: 目前线程运行的g

协程是如何在线程上运行

如下所示 对应线程属于 循环调用方式 不断的调用属于go 协程的业务方法 对应的版本是 go 0.X 版本内
单线程调用形式
go 语言的多线程循环
多线程 抢占的时候 需要进行枷锁的方式 适用于 Go 1.0 版本
操作系统线程执行一个调度的循环,顺序指向Goroutine, 调度循环非常像线程池,如果协程顺序执行,无法并发
问题: 1. 协程顺序执行 无法并发
问题: 2. 多线程并发时候,会导致协程队列的全局锁问题
所以 出现 G-M-P 调度方式

G-M-P调度模型

解决问题:多线程并发的时候,会抢占协程队列全局锁
在本地队列, 每个线程获取全局锁时候获取多个协程任务, 只有一个线程进行访问 不存在锁问题
本地队列P结构体
在这里插入图片描述
P的作用:

  • M与G之间的中间器(送料器): M 表示线程,G 协程任务,P表示本地队列
  • P持有一些G,使得每次获取G不用从全局找
  • 大大降低了并发的冲突问题
  • 如果本地任务和全局任务也没有,go 有窃取任务
窃取形式的工作机制
  1. 新建协程:
  • 随机寻找一个队列P,将新协程放入P的runnext(插队) ,go 语言中新创建协程会立即执行 ,只有P本地队列满时候,放入到全局队列中, 新创建协程属于 newproc函数
    G-M-P 模型有个问题 导致协程的饥饿问题,如何解决这饥饿问题

如何实现协程并发

解决问题:协程顺序执行,无法并发 在go 语言中 支持进行挂起,执行其他的新协程任务,每次线程循环61次后从全局获取任务。

  • 如何进行切换时机
  1. 主动挂起: 业务主动 go park () 方法
  2. 系统调用完成时(existsyscall())
  3. 函数调用的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

  1. 缓冲区, 发送的等待队列,接收的等待队列
  2. go 语言使用 Ring buffer 环形缓冲区, 大幅度降低GC的开销
    在这里插入图片描述
  3. 使用互斥锁保护hchan 的结构体本身
  4. Channel 并不是无锁的, 主要塞入和取数据时候 需要枷锁,其他操作不需要枷锁
    5.Channel 内部数据结构

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

  1. 首先查看是否可以即时执行case
  2. 没有default , 将自己注册在 channel中休眠等待
  3. Time.channel 在倒计时塞入一个数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值