现在的位置: 首页 > 编程语言 > 正文

Go语言的Goroutine和协程是什么

2020年02月20日 编程语言 ⁄ 共 2620字 ⁄ 字号 评论关闭

  今天来学习Go语言的Goroutine机制,这也可能是Go语言最为吸引人的特性了,理解它对于掌握Go语言大有裨益,话不多说开始吧!

  1.协程简介

  大家对于进程、线程都很熟悉,但协程就没有火了,协程并不是Go语言特有的机制,相反像Lua、Ruby、Python、Kotlin、C/C++等也都有协程的支持,区别在于有的是从语言层面支持、有的通过插件类库支持。Go语言是原生语言层面支持,本文也是从Go角度去理解协程。

  1.1 协程基本概念和提出者

  协程英文是Coroutine译为协同程序“

  协同程序是一种计算机程序组件,它允许暂停和恢复执行,从而可以作为通用化的非抢占式多任务处理子程序。

  协同程序非常适合实现例如协作任务、异常、事件循环、迭代器、管道等熟悉的程序组件。

  根据唐纳德·克努特的说法,梅尔文·康威在1958年将Coroutine这个术语应用于装配程序的构建,直到在1963年才首次发表了阐述Coroutine的论文。

  1.2 协程和进线程的对比

  我们来复习一下进线程和协程的一些基本特点吧:

  进程是系统资源分配的最小单位, 进程包括文本段text region、数据段data region和堆栈段stack region等。进程的创建和销毁都是系统资源级别的,因此是一种比较昂贵的操作,进程是抢占式调度其有三个状态:等待态、就绪态、运行态。进程之间是相互隔离的,它们各自拥有自己的系统资源, 更加安全但是也存在进程间通信不便的问题。

  进程是线程的载体容器,多个线程除了共享进程的资源还拥有自己的一少部分独立的资源,因此相比进程而言更加轻量,进程内的多个线程间的通信比进程容易,但是也同样带来了同步和互斥的问题和线程安全问题,尽管如此多线程编程仍然是当前服务端编程的主流,线程也是CPU调度的最小单位,多线程运行时就存在线程切换问题,其状态转移如图:

  协程在有的资料中称为微线程或者用户态轻量级线程,协程调度不需要内核参与而是完全由用户态程序来决定,因此协程对于系统而言是无感知的。协程由用户态控制就不存在抢占式调度那样强制的CPU控制权切换到其他进线程,多个协程进行协作式调度,协程自己主动把控制权转让出去之后,其他协程才能被执行到,这样就避免了系统切换开销提高了CPU的使用效率。

  抢占式调度和协作式调度的简单对比:

  看到这里我们不免去想:看着协作式调度优点更多,那么为什么一直是抢占式调度占上风呢?让我们继续一起学习,可能就能解答这个问题了。

  1.3 实际工作中的我们

  我们写程序的时经常需要考虑的因素就是提高机器使用率,这个非常好理解。当然机器使用率和开发效率维护成本往往存在权衡,说句大白话就是:要么费人力要么费机器,选一个吧!

  机器成本里面最贵的就是CPU了,程序一般分为CPU密集型和IO密集型,对于CPU密集型我们的优化空间可能没那么多,但对于IO密集型却有非常大的优化空间,试想我们的程序总是处于IO等待中让CPU呼呼睡大觉,那该多糟糕。

  为了提高IO密集型程序的CPU使用率,我们尝试多进程/多线程编程等让多个任务一起跑分时复用抢占式调度,这样提高了CPU的利用率,但由于多个进线程存在调度切换,这也有一定的资源消耗,因此进线程数量不可能无限增大。

  我们现在写的程序大部分都是同步IO的,效率还不够高,因此出现了一些异步IO框架,但是异步框架的编程难度比同步框架要大,但不可否认异步是一个很好的优化方向,先不要晕,来看下同步IO和异步IO就知道了:

  同步是指应用程序发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行,异步是指应用程序发起I/O请求后仍继续执行,当内核I/O操作完成后会通知应用程序或者调用应用程序注册的回调函数。

  我们以C/C++开发的服务端程序为例,Linux的异步IO出现的比较晚,因此像epoll之类的IO复用技术仍然有相当大的地盘,但是同步IO的效率毕竟不如异步IO,因此当前的优化方向包括:异步IO框架(像boost.asio框架)和协程方案(腾讯libco)。

  2.Go和协程

  我们知道协程是Coroutine,Go语言在语言层面对协程进行了原生支持并且称之为Goroutine,这也是Go语言强大并发能力的重要支撑,Go的CSP并发模型是通过Goroutine和channel来实现的,后续会专门写一下CSP并发模型。

  2.1 协作式调度和调度器

  协作式调度中用户态协程会主动让出CPU控制权来让其他协程使用,确实提高了CPU的使用率,但是不由得去思考用户态协程不够智能怎么办?不知道何时让出控制权也不知道何时恢复执行。

  读到这里忽然明白了抢占式调度的优势了,在抢占式调度中都是由系统内核来完成的,用户态不需要参与,并且内核参与使得平台移植好,说到底还是各有千秋啊!

  为了解决这个问题我们需要一个中间层来调度这些协程,这样才能让用户态的成千上万个协程稳定有序地跑起来,我们姑且把这个中间层称为用户态协程调度器吧!

  2.2 Goroutine和Go的调度器模型

  Go语言从2007年底开发直到今天已经发展了12年,Go的调度器也不是一蹴而就的,在最初的几个版本中Go的调度器也非常简陋,无法支撑大并发。

  经过多个版本的迭代和优化,目前已经有很优异的性能了,不过我们还是来回顾一下Go调度器的发展历程。

  Go的调度器非常复杂,篇幅所限本文只提一些基本的概念和原理,后续会深入去展开Go的调度器。

  最近几个版本的Go调度器采用GPM模型,其中有几个概念先看下:

  GPM模型使用一种M:N的调度器来调度任意数量的协程运行于任意数量的系统线程中,从而保证了上下文切换的速度并且利用多核,但是增加了调度器的复杂度。

  整个GPM调度的简单过程如下:

  新创建的Goroutine会先存放在Global全局队列中,等待Go调度器进行调度,随后Goroutine被分配给其中的一个逻辑处理器P,并放到这个逻辑处理器对应的Local本地运行队列中,最终等待被逻辑处理器P执行即可。

  在M与P绑定后,M会不断从P的Local队列中无锁地取出G,并切换到G的堆栈执行,当P的Local队列中没有G时,再从Global队列中获取一个G,当Global队列中也没有待运行的G时,则尝试从其它的P窃取部分G来执行相当于P之间的负载均衡。

抱歉!评论已关闭.