入门参考:从 Go 中的协程理解串行和并行
Go语言的设计亮点之一就是原生实现了协程,并优化了协程的使用方式。使得用Go来处理高并发问题变得更加简单。今天我们来看一下Go中的协程。
从串行到并行
在处理器还是单个单核的时候,这个时候并不存在并行,因为只有一个处理器。所以那时候的编程都是串行编程。程序执行都是从头顺序执行到尾。到了多处理器多核的时代,为了充分利用处理器的处理能力,开始出现了并发编程。开发者开始在进程中启用多个线程来执行操作,利用CPU的调度能力来最大化程序处理效率。
并发,并行
在说到并发编程的时候总会遇到这两个概念,面试的时候也会问道,在这里就简单说一下这两者的区别:
并发是一种能力,是指多个任务在一段时间内同时发生。
并行值得是多个任务同时发生,就是并行。
并发值得是并行的能力,并发不一定是同时发生,可能是同一时间段内交替发生。
进程,线程,协程
进程和线程是操作系统的基本概念:
进程:指计算机中已运行的程序,进程是程序的基本执行实体。
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程的实际运行单位。
那么协程是在线程之上,更加轻量级的设计。协程因为只工作在用户控件,没有线程上下文切换带来的消耗。协程的调度由用户手动切换,所以更加灵活。
协程的另一大优势就是因为在用户空间调度,所以不会出现代码执行一半被强制中断,所以无需原子操作锁。
Go中的协程
在Go中使用协程非常简单,就使用go关键字就可以了。我们来看一段串行代码使用协程如何进行操作:
那么使用协程,我们来看一下运行结果:
我们可以看出使用go关键词后,打印并不是按照顺序串行执行的,而是在主协程执行结束后,打印协程才开始执行。
Go协程的调度机制
Go中的协程调度模型是G-P-M模型:
G代表Goroutine,也就是Go中的协程对象。
P代表Processor,代表虚拟的处理器。一般来说,和逻辑核一一对应。
M代表Machine,实际上是操作系统的线程。
这里我们简单说一下Go的调度机制,感兴趣或者有了解的可以自行看Go的源码:
在Go程序启动时,会给每个逻辑核分配一个P(虚拟处理器)
同时,Go会创建一个主协程G,来执行程序。新创建的G会被放到LRQ(P上的本地G队列)或者GRQ(全局G队列)。
给P分配一个M(内核线程),这些M由OS Scheduler调度而非Go Scheduler调度。M用来运行G
P会尽可能获取G来运行,当没有G运行后,会销毁并重新进入调度
其中第4条 尽可能获取G
则是Go的有趣的设计理念之一,当一个 P 发现自己的 LRQ 已经没有 G 时,会从其他 P “偷” 一些 G 来运行。看看这是什么精神!自己的工作做完了,为了全局的利益,主动为别人分担。这被称为 Work-stealing
。
再看串行和并行
这里我们以Go协程来继续说一下串行和并行,对于习惯于串行编程的程序员来说,理解并行可能稍微需要点时间,对于程序设计来说,并行的设计主要是为了提高程序运行的效率,使得程序能够充分利用多核多处理器的资源(或者多机器)。那么对于如何充分利用,大部分支持并行编程的语言都有其内部的调度机制,即使没有,也会使用系统的调度机制--线程调度。
那么对于并行调度机制总体上分为两类:协作式和抢占式
协作式:一个任务得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU ,所以任务之间需要协作使用一段时间的 CPU ,放弃使用,其它的任务也如此,才能保证系统的正常运行。如果有一个任务死锁,则系统也同样死锁。
抢占式:总控制权在操作系统手中,操作系统会轮流询问每一个任务是否需要使用 CPU ,需要使用的话就让它用,不过在一定时间后,操作系统会剥夺当前任务的 CPU 使用权,把它排在询问队列的最后,再去询问下一个任务。如果有一个任务死锁,系统仍能正常运行。
在 Go1.1 版本中,调度器还不支持抢占式调度,只能依靠 goroutine 主动让出 CPU 资源,存在非常严重的调度问题。
Go1.12 中编译器在特定时机插入函数,通过函数调用作为入口触发抢占,实现了协作式的抢占式调度。但是这种需要函数调用主动配合的调度方式存在一些边缘情况。
后面Go在1.14版本实现了基于信号的真抢占式调度。用于解决解决了垃圾回收和栈扫描时存在的问题。
Go的协程调度目前虽然不能称得上完美,但是对于我们理解并行有一定的帮助。所谓并行编程,就是开启多个任务而不用等待任务结果。可以使得相互独立的任务同时运行,比如文件写入等。
版权声明: 本文为 InfoQ 作者【soolaugust】的原创文章。
原文链接:【http://xie.infoq.cn/article/420f356c48862295e1798b877】。未经作者许可,禁止转载。
评论