goroutine实践分享
创始人
2024-05-31 21:12:34
0

一.goroutine原理及GPM调度模型

  • 架构图

    GPM

  • 并发模型

​ 并发模型分为用户线程模型、内核线程模型、两级线程模型

​ 用户线程模型是用户线程和内核线程一对多映射模型。多个用户线程绑定到一个内核线程上,线程调度由用户线程库实现,系统对线程无感知,该模型线程调度由于不需要用户态到内核态切换,实现起来比较轻量,资源消耗少,但该模型中所有线程都绑定到同一个内核线程上,并没有实现真正的并发。

​ 内核线程模型是用户线程和内核线程一对一映射模型。内核线程模型实现相对简单,一个用户线程绑定到一个内核线程上,线程调度由系统实现,实现了真正意义上的并发,但在不同线程做切换需要用户态到内核态切换,资源消耗较大,对性能影响很大。

​ 两级线程模型是用户线程和内核线程n对m映射模型。该模型中n个用户线程和m个内核线程实现动态绑定,实现了系统级和用户级的两级调度模型,系统调度器调度内核线程,用户调度器调度用户级线程保证每个用户线 程公平调度,该模型实现了内核线程池的效果,可以大大减少内核线程创建、销毁的资源消耗和性能损失。

  • G-P-M模型

​ goroutine实现了两级线程模型,每一个go关键字都创建一个用户级线程,每一个被创建的线程都是一个可调度的执行单元。内核线程创建需要2mb的固定栈空间,而每一个go线程只需要2kb的栈空间,随着任务的执行,栈空间不足时,调度器会动态的给go线程分配另一块连续栈空间。内核线程的调度需要用户态到内核态的切换,非常耗费资源,而go线程调度由runtime在用户级别细线,资源消耗很少。

​ goroutine模型,抽象为三个结构G、P、M,G代表一个用户级别线程,G保存任务执行相关的堆栈和上线文状态。P代表一个逻辑处理器,每个P上都有一个G执行队列,P的数量决定了系统最大并行度,P的存在达到了内核线程池的效果。M代表一个系统线程,每个M在绑定一个P后开始执行上面的G,M不保存G的状态,这是G可以跨M调度的基础。

  • G-P-M调度

​ goroutine队列分为global任务队列和P维护的local任务队列。当通过go关键字创建一个新的goroutine的时候,它会优先被放入P的本地队列。为了运行goroutine,M需要持有(绑定)一个P,接着M会启动一个OS线程,循环从P的本地队列里取出一个goroutine并执行。当然还有上文提及的 work-stealing调度算法:当M执行完了当前P的Local队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从Global队列寻找G来执行,如果Global队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。

  1. 用户态阻塞处理
    goroutine因为channle或sync同步阻塞时,该goroutine会被放置到某个wait队列,该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有runnable的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时,G被标记为runnable,尝试加入G2所在P的runnext,然后再是P的Local队列和Global队列。
  2. 内核态阻塞处理
    当G被阻塞在某个系统调用上时,此时G会阻塞在_Gsyscall状态,M也处于 block on syscall 状态,此时的M可被抢占调度:执行该G的M会与P解绑,而P则尝试与其它idle的M绑定,继续执行其它G。如果没有其它idle的M,但P的Local队列中仍然有G需要执行,则创建一个新的M;当系统调用完成后,G会重新尝试获取一个idle的P进入它的Local队列恢复执行,如果没有idle的P,G会被标记为runnable加入到Global队列。

二.应用实践

  • routinepool

    go协程虽然相比系统线程创建、销毁轻量仅需要2kb栈空间,协程间调度不需要用户态到内核态切换,但是如果大量频繁创建、销毁协程如10w、100w,则内存的消耗、协程调度cpu消耗仍需要浪费大量系统资源。这些消耗的资源大部分浪费在协程的创建、销毁上面,对此可以使用协程池技术,达到协程资源复用目的。

    routinepool协程库实现了固定协程池和弹性协程池两种协程池,简要介绍整体结构。

    协程池

type RoutinePool struct {......}func NewFixedRoutinePool(taskQueueSize int64, waitInterval time.Duration,normalWorkerSize int64) (rp *RoutinePool){ ...... }func NewCachedRoutinePool(taskQueueSize int64, waitInterval time.Duration,normalWorkerSize int64, maxWorkerSize int64) (rp *RoutinePool) {  ......  }func (rp *RoutinePool) Execute(task Runable) bool { ...... }func (rp *RoutinePool) ExecuteFunc(call func()) bool { ...... }func (rp *RoutinePool) Close() {  ......  }

​ 可执行任务接口

type Runable interface {Run()}

​ 工作协程

type Worker struct {......
}func NewWorker(tq chan Runable, ei time.Duration, rf bool, dc chan bool, cc chan struct{}) (w *Worker) {  ......  }func (w *Worker) Start() { ......  }

​ RoutinePool 对象实现了协程池管理功能,可以使用NewFixedRoutinePool创建固定协程池、NewCachedRoutinePool创建弹性协程池。执行的任务可以是实现Runable的接口或func()类型函数,通过Execute或ExecuteFunc函数放入协程池,等待worker队列执行。

  • gofuture

    go协程池使用虽然方便,但是创建后不容易控制,不能方便的终止其执行,也不能类似函数调用便捷的获取其执行结果。gofuture库对默认的go协程使用做了一层封装,可以控制协程的执行,获取协程执行结果。

    type Interface interface {Cancel()IsCancelled() boolGet() (interface{}, error)GetUntil(d time.Duration) (interface{}, bool, error)}func NewFuture(inFunc func() (interface{}, error)) Interface { ...... }
    

    NewFuture 产生一个内部实现了Interface接口的future对象。该对象提供了Get、GetUntil、Cancel、IsCancelled函数调用,Get函数获取inFunc执行结果;GetUntil函数同样是获取inFunc执行结果,但在inFunc未执行前会等待d时间段,超时则错误返回;Cancel函数取消inFunc任务执行;IsCancelled函数判断任务是否取消执行。

  • goscheduled

    在编程中经常会遇到某一类任务按照固定频率或者固定时间段多次执行,为此可以实现一个时间优先任务调度队列。

    队列管理对象

    type GoScheduled struct {......}func NewGoScheduled() (gs *GoScheduled) { ...... }func (gs *GoScheduled) ScheduledAtFixedRate(task routinepool.Runable, i time.Duration) bool { ......  }func (gs *GoScheduled) ScheduledAtFixedDelay(task routinepool.Runable, i time.Duration) bool { ...... }func (gs *GoScheduled) Close() { ...... }
    

    调度任务队列

type ScheduledQueue struct {......}func NewScheduledQueue() (sq *ScheduledQueue) { ...... }func (sq *ScheduledQueue) HeapPush(x interface{}) { ...... }func (sq *ScheduledQueue) HeapPop() (x interface{}) { ...... }

​ 调度任务

 	type Scheduled struct {......}func (s *Scheduled) Run() { ...... }

​ GoScheduled 实现了任务调度模块管理,可以通过ScheduledAtFixedRate函数调度固定频率任务的执行,ScheduledAtFixedDelay函数调度固定时间段间隔任务的执行。ScheduledQueue 使用优先队列实现任务按照时间优先级进行调度,HeapPush函数实现调度任务入队,HeapPop函数实现调度任务出队。Scheduled 实现一个具体可以被调度的任务。

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...