新公司使用 Golang,Golang 的魔力之一就是可以开启成千上万的 goroutine 来处理并发,于是上网看一些简单的关于 Goroutine 的介绍 https://blog.nindalf.com/post…
后期再深入了解 Go Runtime 是如何管理和调度 goroutine
Go 语言介绍
如果你第一次接触 GO 编程,或者你对“并发不是并行这句话”没有任何概念,你可以先去看一下 Rob Pike 的演讲excellent talk on the subject,这30分钟的演讲你值得拥有
当人们听到 concurrency(并发) 这个词往往会联想到 parallelism(并行),他们是有关联但完全不一样的概念。在编程的世界中,concurrency 是独立执行的过程的组合,而 parallelism 则是计算任务的同时执行。concurrency 是在一段时间内处理多个任务,parallelism 是同时做多个任务。
Go 可以让我们去编写并发的程序,Go提供了 goroutine 和 goroutine 之间相互通信的能力。本文会更多地关注 goroutine
Goroutine 和 OS Thread
Go 使用 goroutine 处理并发,而 Java 则使用 thread(线程)。为了比较 goroutine 和 thread 的区别,我们关注以下三个方面——内存使用,开启和销毁,切换时间
内存使用
创建 goroutine 不需要太多的内存,2KB的栈内存足矣,随后会伴随堆内存的分配和释放而增长。线程的开启则需要1MB内存 (goroutine的500倍),并伴随着一片警戒内存页(guard page)的分配,作为线程栈内存之前的警戒区域。
所以,服务器在接收请求的时候可以为每个请求分配一个 goroutine 来处理,并不会有内存问题。但是一请求一线程 (例如Java bio 编程) 模式很有可能会导致 OutOfMemoryError,这不仅仅出现在 Java (bio) 编程中,那些使用OS线程处理并发的语言大多都需要面临这类问题。
Goroutine 创建和销毁
线程的创建和销毁有很大的开销,因为我们必须向 OS 请求线程资源,并且在线程完成后归还,维护线程池是一个很好的应对方法。相反,Go Runtime 花费很小的开销就能创建和销毁 goroutine,Go语言也没有提供对 goroutine 的人工管理接口 (意思就是 Go 已经安排好了,你不用瞎操心去管理 goroutine)
Goroutine 切换开销
当线程阻塞时,另一个线程会被调度。线程的调度是抢占式的,在切换过程中,调度器必须保存和恢复所有寄存器状态,包括:16个通用寄存器,程序计数器 PC,栈指针 SP,段寄存器,16个 XMM 寄存器,16个 AVX 寄存器,FP 协处理器状态等等。系统频繁地进行线程切换将会带来巨大的开销。
Goroutine 的调度是协作式的(所以被称为 go 协程?)。在进行 goroutine 切换时,只有3个寄存器需要被存储和恢复,他们是程序计数器 PC,栈指针和通用寄存器 DX,开销很小。
Goroutine 的数量通常是巨大的,但这不会影响 goroutine 的切换时间。调度器只会关注运行状态的 goroutine,而忽略阻塞态的 goroutine。Go 跟现代调度器一样都是 O(1) 单位时间复杂度,意味着增加 goroutine 数量不会增加切换时间
Goroutine 如何执行
之前提及过,Go Runtime 管理了 goroutine 的创建,调度和销毁。Go Runtime 事先分配了一些线程,所有的 goroutine 都在这些线程上多路复用。在某个时刻,每个线程只会装载执行一个 goroutine,如果那个 goroutine 被阻塞了,他会被换出,并由另一个 goroutine 获得线程执行
因为 goroutine 的调度是协作式的,一个执行无限循环任务的 goroutine 会“饿死”其他在相同线程上的 goroutine(其他 goroutine 无法抢占获得线程)。这个问题在 Go 1.2 中有所缓解,通过在 goroutine 进入一个方法时偶尔去调用调度器(执行调度),所以一个包含方法执行的无限循环是可被抢占的。
Goroutine 阻塞
Goroutine 的阻塞不会导致线程的阻塞。即使成千上万的 goroutine 被创建,即使他们大多数都被阻塞了,但只要 Go Runtime 调度其他可用的 goroutine,就不会造成系统资源浪费
用简单的话说,goroutine 是一种更轻量级的 OS 线程的抽象。Go 开发者不需要管理线程,同样OS也感知不到 goroutine 的存在。在 OS 的视角下,Go 程序的就像一个事件驱动的 C 程序。
线程和CPU
虽然你不能直接控制 Go Runtime 创建线程的数量,你仍可控制程序使用的 CPU 核心数,通过runtime.GOMAXPROCS(n)
来设置 GOMAXPROCS。增加 CPU 核心数也许并不能显著提高程序的性能,但你可以使用工具来找到程序运行最理想的 CPU 核心数
总结
像其他语言一样,你应该尽可能避免让多个 goroutine 同时访问共享资源。goroutine 之间不要使用共享内存进行通信,最好的做法是使用 channel 在他们之间传输数据。
最后我强烈建议你阅读 C.A.R.Hoare 的文章 Communicating Sequential Processes。在文章中,他预言单核心 CPU 会最终到达性能瓶颈,芯片制造者将会堆积核心数量。他所表达的观点对 GO 语言的设计有着深远的影响。