• 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工作时候,向目标线程发送信号
    
    • 1
    • 2

    协程太多问题

    • 在主协程中不断的产生协程,too many socket 连接出问题
    • 文件打开限制,调度太大
      解决问题:
    • 优化业务的逻辑
    • 协程池(不建议使用,Go语言线程 已经相当于池化,不要在二级池化,go 语言初衷 希望协程即用即销毁,不要池化
    // 预先创建一定数量协程, 将任务创建送入到等待的队列
    不推荐使用
    
    • 1
    • 2
    • 利用channel 缓存区 牺牲并发的性能
    func do (i int , ch chan struct{}){
    	fmt.Println(i)
    	time.Sleep(time.Second)
    	<- ch
    }
    // 利用 channel的缓冲区,控制协程产生的数量,一旦缓冲区被阻塞了, 不会产生更多的协程
    // 适合场景 大量批量的设置协程
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    高并发下通信方式 channel

    • 无缓冲区 channel 接收必须要发送前不然,导致缓冲区阻塞
    针对 无缓冲区的 管道,必须要先从管道里面获取接受数据,才能进行数据塞入,所以对于无
    缓冲区的管道 使用 协程接收在发送的前面 不然导致阻塞
    
    • 1
    • 2
    • 共享内存方案
      在使用共享方案时候 需要不断的查询 共享变量的值, 如果使用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 在倒计时塞入一个数据
  • 相关阅读:
    SQL零基础入门教程,贼拉详细!贼拉简单! 速通数据库期末考!(十)
    【mybatis基础(二)】实现对数据库的CRUD
    B树和B+树
    我3年前写的博客,又被别人抄去发论文了,该论文整个正文部分几乎直接照抄我的博客
    HTTP协议发展史
    【regex】正则表达式
    Elasticsearch聚合----aggregations的简单使用
    tomcat面试和Spring的面试题
    AD9371 Crossbar
    rust calcmine读取excel
  • 原文地址:https://blog.csdn.net/qq_27217017/article/details/126800448