Go协程的简单介绍

一.进程,线程,协程的基本概念

        进程指的是一个在内存中运行的程序,是应用程序的启动实例,每一个进程都有独立的内存空间,不同的进程通过进程的通信方式来通信.进程是资源分配的最小单位.

        线程从属于进程,一个进程至少会包含一个线程,线程是CPU能够进行运算调度的基本单位.一个进程中可以有多个线程,而这多个线程之间可以共享这个进程的资源并通过共享内存等线程间的通信方式来通信.

        协程又被称为微线程,是一种用户的轻量级线程,但是与线程相比,协程不受操作系统的调度,协程的调度是由开发人员决定的.Go语言是在语言层面上直接支持协程的,在Go的应用程序中,开发人员可通过go 关键字创建协程.

二.协程的优势

        多线程编程需要谨慎处理线程的同步问题,而且因为调度程序的任何时候都能中断线程,必须牢记保留锁,去保护程序中的重要部分,防止多线程在执行的过程中中断.

        但协程是用户自己来编写调度逻辑的,不需要CPU考虑怎么调度,节省了一定的开销,并且协程默认会做好全方位防护,防止中断.

        并且在高并发的场景中,频繁创建线程会造成不必要的开销.这一点可以通过线程池的技术改变.

在线程池中预先保留一定数量的线程,新的任务不再以创建线程的方式去执行,而是将任务发布到任务队列当中,线程池中的线程不断地从任务队列中取出任务并执行,有效减少了线程的创建和销毁所带来的开销.

        但线程池会带来其他的问题,下面介绍一下.

        

         线程池中的线程会不断的从任务队列中取出任务并执行,而worker线程则交给了操作系统调度.如果线程中执行的G任务发生了系统调用,则操作系统会将该线程置为阻塞状态,消费任务列队中的线程减少,线程池中的消费能力变弱.如果线程池中的大部分任务都进行了系统调用,则会让这种状态持续恶化,大部分线程都处于阻塞状态,任务队列中的任务产生堆积. 虽然可以通过增加线程池中的线程数量改观这一情况,但是随着线程数量的不断增加,过多的线程会争抢CPU资源,会出现线程不断增加,消费能力却逐渐减弱的现象

        

         但是工作在用户态的协程能大大减少上下文切换所产生的开销.协程调度器能够把可运行的协程逐个调度到协程中,同时及时把阻塞的协程调度出线程,有效的避免了线程之间频繁切换的问题,实现了使用少量线程实现高并发的效果.

        多个协程分享操作系统分给线程的时间片,从而达到充分利用CPU算力的目的,协程调度器则决定了协程运行的顺序.

线程运行协程调度器指派的协程,但每刻只能运行一个协程.

        

三.调度模型

        1.线程模型

线程可以分为用户线程和内核线程,用户线程由用户创建,同步和销毁,内核线程则由内核来管理.根据用户线程管理方式的不同,可以分为三种线程模型.

        1.N:1模型,即N个用户线程运行在一个内核线程中,用户线程切换上下文效率快,但是无法利用现在多核CPU的算力.

        2.1:1模型,每一个用户线程对应一个内核线程,充分利用了多核CPU的算力,但是用户线程上下文切片效率低.

        3.M:N模型:Go实现的就是M:N模型,可以理解为前两种模型的结合,M个用户线程(协程)运行在N个线程中,充分利用了多核CPU的算力,并且协程上下文切换也很快,但是该模型的调度算法比较复杂.

        2.Go调度器模型

Go调度器模型中包含三个关键实体.

        1.machine: 工作线程,由操作系统调度

        2.processor: 处理器,此处不是指CPU,而是一个GO定义的概念,包含了运行Go代码的必要资源,也有调度goroutine的能力.

        3.goroutine:Go协程,每一个go关键字都会创建一个协程.

工作线程必须持有处理器processor才可以执行代码,跟系统中的其他线程一样,工作线程machine也会被系统调用阻塞.处理器的个数在程序启动时决定的,默认是等于CPU的核数,可以通过环境变量中的GOMAXPROCS或者在程序中使用 runtime.GOMAXPROCS()方法指定p的个数.

runtime.GOMAXPROCS(80) //使用runtime.GOMAXPROCS()方法设置GOMAXPROCS为80
#通过环境变量设置
$go version
go version go1.9.4 darwin/amd64
$export GOMAXPROCS=800; go run maxprocs.go
GOMAXPROCS: 800
$export GOMAXPROCS=4; go run maxprocs.go
GOMAXPROCS: 4

工作线程machine的个数通常稍微大于处理器processor的个数,因为除了运行go代码,runtime包还有其他内置任务需要处理.

      这个是一个简单的调度器模型,包含了两个工作线程M,每一个M持有一个处理器P,每一个工作线程中都有一个协程G在运行,还有一些协程在队列中等待并P调度,执行.而全局的协程队列是由多个处理器P共享.Go1.1之后,引入了runqueues,每一个处理器P访问自己的runqueues(等待被调度的协程队列)不需要加锁.

        处理器P中的协程G额外的再创建的子协程会加入到本地的runqueues,但是本地队列已满,或者阻塞的协程被唤醒,则协程会被放到全局的runqueues中,处理器处理调度本地的runqueues中的协程,还会周期性的从全局的runqueues中摘取协程去调度.

        3.调度策略

                1.队列轮转

        每一个处理器P维护一个协程G队列,处理器P依次将协程G调度到M中执行.协程G执行结束后,处理器P会再次调度一个协程G到M中执行.同时,每个P会周期性地查看全局队列中是否有G待运行,并将其调度到M中执行,全局队列中的G主要来自从系统调用中恢复的G.

                2. 系统调用

        线程在执行系统调用时,可能会被阻塞,对应到调度器模型,如果一个协程发起了系统调用,那么对应的工作线程会被阻塞,这样会导致处理器P的runqueues队列中的协程将得不到调度,相当于队列中的所有协程都会被阻塞.

        此时在上面说过的工作线程的数量一般会略大于处理器P的数量这句话就派上用处了.多出来的工作线程M将会在协程G产生系统调用时发挥作用.与线程池类似,Go也会提供一个M的池子,需要时从池子中获取,用完之后再放回池子,不够时就再创建一个.

       M0由于陷入系统调用而被阻塞,M1接替M0的工作,只要P不空闲,就可以充分利用CPU.

冗余的M的来源可能是缓存池,也可能是新建的.当G0结束系统调用后,根据M0是否能获取到P,对G0进行不同的处理.

        1.如果有空闲的P,则获取一个P,继续执行G0.

        2.如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度.M0进入缓存池睡眠.

        

        3.工作量窃取

        通过go关键字创建的协程通常会优先放到当前协程对应的处理器队列中,可能有些协程自身不断创建新的协程,而有些协程不会创建子协程.这样会导致多个处理器P中委维护的协程队列是不均衡的,结果就是部分处理器非常繁忙,而部分处理器P怠工的情况.

        Go调度器会通过工作量窃取策略的策略解决这一情况.即当某个处理器P没有需要调度的协程时,会从其他处理器中偷取协程.

        4.抢占式调度

抢占式调度指避免某一个协程长时间执行,而阻碍其他协程被调度的机制.

调度器会监控每一个协程的执行时间,一旦发现一个协程执行时间过长且有其他协程再等待时,会把当前协程暂停,调度正在等待的协程,达到一种类似于时间片轮转的效果.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值