goroutine实践分享

一.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() bool
    
       Get() (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 实现一个具体可以被调度的任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值