前言
虽然写了不少go的代码了,但是对于go本身的了解还处于比较片面的阶段,因此在后续几天内打算将基础慢慢补回来。
就从goroutine部分开始整理吧
什么是协程
多核的流行
由于单核性能提升的成本越来越高,出于性能和成本的考虑,多核心逐渐成为主流。但虽然硬件上多核已经有了实现,能充分利用多核特性的程序比例仍然比较小。
进程、线程、协程
多进程、线程或协程都可以利用多核心的特性,但是他们具有不同的特点:
进程是资源分配的最小单位,同一时间执行的进程数不会超过核心数目,由于进程的上下文切换需要切换页目录、内核栈和硬件上下文,其上下文切换开销较高。同时页缓存的切换会导致cache失效,使得cache的命中率降低。
线程上下文切换不需要切换页目录(虚拟内存空间相同),而只需要切换内核栈和硬件上下文,因此其开销低于进程,相对来说更加轻量。多核中线程一般是利用开发者接口,由内核完成到处理器的映射,实现并行。但由于内核栈和硬件上下文的切换仍然需要操作系统协助,需要陷入内核态,因此依旧存在一定开销。
协程占用资源小,由用户进行调度(内核无感知),因此不必陷入内核中(没有内核上下文切换)。由于通常情况下用户调度会使用比线程更轻量的上下文(通常是更少的寄存器和动态大小的栈),因此创建开销以及上下文切换开销会比线程更低。除此之外,同一线程上的协程不会出现写变量冲突(因为它们本身就不会同时执行),因此控制共享资源不必加锁而只要判断状态。
如何根据情况来使用
多进程(稳定性):由于进程通常各用独立的内存区间,进程的崩溃不会影响到其它进程的运行,因此具有更好的稳定性,相比之下线程和协程的崩溃会导致进程崩溃,进而影响到同进程下的其它线程或协程(因为它们共享相同的资源)。
多线程(并行计算任务):多核处理器情况下,并行依靠线程,由内核完成线程到处理器映射的。协程的轻量化设计是为了降低上下文切换开销,并不会在并行计算上带来优势(同一线程上的协程同时只会有一个在执行,因此单线程上开大量协程并不能提高计算的并行度)。更加轻量的上下位在计算过程可能反而引起额外开销(动态栈或共享栈的设计引发的额外栈检查、栈内存分配和栈内存复制操作等)
协程(阻塞频繁的任务):不必进入内核态和更轻量的上下文使其在面临阻塞的时候被调度的开销更低,因此当任务存在大量频繁的阻塞和唤起时,协程的开销会小于线程(提高了并发度,而非并行度)。同时由于同线程内的协程不会出现写变量冲突。
三者的使用中就个人理解:进程倾向于隔离(安全性),线程倾向于并行,而协程倾向于异步。
协程的特点
综上总结之后,协程的特点如下:
- 由用户自己进行调度,减少上下文切换,提高效率
- 栈更小,在同内存中可以比线程开启更多的数量
- 协程在同一线程上,可以避免竞争关系而使用锁
- 适用于被阻塞且需要大量并发的场景,但不适用于大量计算并发场景,计算型场景更适合使用线程实现
goroutine
goroutine有些类似于协程,但它不像是nodejs那种单线程下的携程,它是基于内核线程上的轻量抽象,使用GMP模型抽象(下详述)。
goroutine的调度方式是协同式的,在协同式调度中,没有时间片的概念。为了并行执行goroutine,调度器会在以下几个时间点对其进行切换:
- Channel接收或者发送会造成阻塞的消息
- 当一个新的goroutine被创建时
- 可以造成阻塞的系统调用,如文件和网络操作
- 垃圾回收
尽管调度方式是协同式的,但是go考虑到协程饿死的情况,针对其抢占机制(详见参考资料《Implemention of golang》),同时由于我们不能保证goroutine全都在同一个线程上,因此它不具有协程中不会引起冲突的特点,开发时我们仍需要考虑临界区等因素。
相比nodejs的回调函数方式解决异步的处理方式带来的繁杂嵌套,golang使用了goroutine+channel的设计思路,让异步代码编写时能以一种更加线性的思维来完成,降低了开发人员的精神负担。
GMP模型
Golang的调度器中存在三个概念:
- Processor(P)
- OSThread(M)
- Goroutines(G)
详情待续,也可参考参考资料中《Go的并发机制:线程模型》
参考资料
待看
已参考
进程、线程、协程
进程、线程上下文切换的开销
多进程编程和多线程编程优缺点
微信libco协程介绍
Golang协程详解和应用
Golang 之协程详解
Go语言中Goroutine与线程的区别
Go语言程序设计 读书笔记
Go语言的栈空间管理
Go 运行程序中的线程数
Implemention of golang