在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
0x00.前言老幺小说网 https://laoyao.org前面写了一篇初识Go语言和大家一起学习了Go语言的巨大潜力、语言简史、杀手锏特性等,感兴趣的读者可以回顾一下。 今天来学习Go语言的Goroutine机制,这也可能是Go语言最为吸引人的特性了,理解它对于掌握Go语言大有裨益,话不多说开始吧! 通过本文你将了解到以下内容:
0x01.聊聊协程大家对于进程、线程二位明星都很熟悉,但协程就没有火了,是协程不是携程哦! 协程并不是Go语言特有的机制,相反像Lua、Ruby、Python、Kotlin、C/C++等也都有协程的支持,区别在于有的是从语言层面支持、有的通过插件类库支持。Go语言是原生语言层面支持,本文也是从Go角度去理解协程。 1.1 协程基本概念和提出者协程英文是Coroutine译为协同程序,我们来看下维基百科对Coroutine的介绍: Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. 简单翻译一下: 协同程序是一种计算机程序组件,它允许暂停和恢复执行,从而可以作为通用化的非抢占式多任务处理子程序。 协程的提出者梅尔文·爱德华·康威是一位计算机科学家,除了协程之外他还创造了Conway's Law康威定律,他基于社会学观察提出了系统设计的一些观点,本文就不展开了,感兴趣的可以看下作者的论文How Do Committees Invent?: http://www.melconway.com/Home/Committees_Paper.html 1.2 协程和进线程的对比我们来复习一下进线程和协程的一些基本特点吧: 进程是系统资源分配的最小单位, 进程包括文本段text region、数据段data region和堆栈段stack region等。进程的创建和销毁都是系统资源级别的,因此是一种比较昂贵的操作,进程是抢占式调度其有三个状态:等待态、就绪态、运行态。进程之间是相互隔离的,它们各自拥有自己的系统资源, 更加安全但是也存在进程间通信不便的问题。
协程在有的资料中称为微线程或者用户态轻量级线程,协程调度不需要内核参与而是完全由用户态程序来决定,因此协程对于系统而言是无感知的。协程由用户态控制就不存在抢占式调度那样强制的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)。
0x02.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执行即可。 Goroutine在整个生存期也存在不同的状态切换,主要的有以下几种状态:
画个状态图看下:
0x03.巨人的肩膀https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/ https://www.flysnow.org/2017/04/11/go-in-action-go-goroutine.htm 人类身份验证 - SegmentFault https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.2.html https://wudaijun.com/2018/01/go-scheduler/ 就想叫yoko:[译] Go语言调度器 |
请发表评论