原文地址
我没去获得授权来着,随手翻译一下就当做笔记了
如今 golang 获得了从前不可想像的流行度,其主要原因是它那由 goroutines
与 channels
组合所提供简单且轻量的并发开发体验。
并发已经存在于线程很长一段时间,最近几乎所有应用都使用到了这一特性。
要去了解为什么 goroutines
并不是轻量化的线程,我们需要先了解线程如何在 OS
上进行工作。
如果你已经对线程很熟悉了,你可以直接跳转到这。
什么是线程?
一个线程既是可被一段可被处理器所执行的指令顺序,线程 ( Thread ) 比进程 ( process ) 更加轻量,所以我们可以产生并使用很多线程。
一个生动简单的的例子即 web 服务器。
一个 web 服务器必须得设计成去可在同一时间内处理多条不同的请求 。而且通常这些请求不会依赖对方。
所以线程就会被创建( 或者从线程池内被取出 ),然后请求被委托给线程,已处理并发的情况。
现代处理器可以一次执行多个线程(多线程),并且切换不同的线程来实现并行性。
线程比进程轻量吗?
是,也不是。
要先了解一下两个概念:
- 线程分享内存,所以当它们被创建的时候,不需要创建新的虚拟内存空间,所以也并不需要用到 MMU(
memory management unit
内存管理单元) 来进行上下文切换。 - 线程之间的交流使用的
分享内存
方法比起进程之间的交流需要各种方法如IPC
(Inter-Process Communications
进程内通讯) 的 信号 ,消息队列,管道等,都显得更加的轻量化。
所以说,多进程处理在多核处理器中并非是保证高性能的不二法宝。
再比如 , Linux 不区分线程以及处理器,并把他们都称为 多任务 (Tasks)
。当它们被克隆的时候,每一个任务都有其最大到最小的分享等级。
当你调用 fork()
的时候 , 一个新的,没有分享的文件描述符,没有 PIDs
, 没有内存空间的任务(task)
会被创建 。当你调用 pthread_create()
时,一个新的任务会被创建,并包含所有以上所被分享的资源。
当然,使用 分享内存 来同步化数据 以及 在多核情况下使用 L1 缓存 会造成比起在 不同线程上不同内存来同步时 更大的开销。
Linux 开发者们一直在致力于用最小的开销来切换任务,并且他们成功了。创建一个新的任务同窗比创建新的线程花销更大,但是切换任务却并非如此。
还有什么可以对线程进行提升?
这里有 3 种情况会让线程变慢:
线程因为巨大的栈大小(>= 1 MB ) 而消耗了过多的内存。所以创建 1000 个以上的线程即代表你需要整整 1 GB 的内存。
线程的回复 需要 一系列的寄存器,包括
AVX(Advanced Vector extension)高级向量拓展指令集
,SSE(Streaming SIMD Ext.流式单指令流多数据流拓展)
, 浮点寄存器 ,程序计数器PC(Program Counter)
,栈指针Stack Pointer(SP)
,等,都会对应用程序的性能造成影响。 3. 线程的创建与线程的销毁需要对系统资源(比如内存)进行的调用,会非常的慢。
Goroutines
Goroutines
只存在于 go runtime
的虚拟空间内,而不在 OS
中。
所以, Go 的运行调度器需要管理其生命周期。
Go Runtime 调度器管理着 三个C
结构体:
- G: 相当于单个
go routine
, 并且包换了栈指针,基于栈,由它的ID
, 缓存,以及状态。 - M: 代表了一个系统线程,其同时包含了一个全局可运行
goroutines
队列 的指针,当前正在运行的goroutine
, 以及所关联的调度器。 - 调度架构:一个全局的结构体,和线程一样,但它包含的是空闲以及等待的
goroutines
的队列。
所以,在启动的时候, go runtime
会启动一定数量的 goroutines 来处理GC
, 调度器,以及用户代码,一个OS
的县城会被创建以处理这些 goroutines
. 这些线程的数量将等同于GOMAXPROCS
。
从下面开始!
一个 goroutine 被创建的时候仅仅占用 2kb 的栈大小。 , 每个 go 函数都已经做好了是否需要更多栈,以及栈是否能被其他两倍内存大小的内存来源所复制的检查。这使 goroutines
在资源使用上更加轻量。
阻塞是无妨的!
如果一个 goroutine
在系统上的调用了,他会阻塞整个线程。但其他线程会从调度器上取出等待队列,并且用其他可运行 goroutines
来执行。
然而,如果你使用 Channel
这种只存在于虚拟空间的方式来沟通,系统并不会阻塞线程。 这样的 gorouintes
简化了 go 在等待阶段,以及其他可运行 goroutines
(在 M
内) 的调度。
请勿中断!
go 的运行时调度器做到了合作的调度,这意味着其他的 goroutines
只会因为当前使用的 goroutine
在阻塞或者已经完成时才会调用 , 如以下情况:
- 当这些操作会造成阻塞时,
Channel
的发送或者接收。 Go
的声明阶段,因为这并不会保证新的goroutine
会被直接调度。- 阻塞的系统调用,比如文件以及网络操作。
- 被
gc
过程所停止之后。
这比基于时序系统(每 10ms 一次)的强占调度要更好,后者会造成阻塞,并调度一个新的线程。这可能会导致任务花更多的时间在 线程数增加 以及在处理 在有低优先级的任务运行时 的 更高优先级的任务的调度。
另外的好处是,因为这些逻辑都是在代码中隐式调用的,当睡眠以及 Channel
等待时,便一直只需要确保/恢复以下几个寄存器。在 Go 中,这意味着在进行上下文切换时仅仅需要调度 3 个寄存器:PC
, SP
, DX(Data Registers)
,而不是需要所有的寄存器,如(AVX
, 浮点寄存器, MMX
)。
如果你打算浏览更多关于 go 的并发原理, 你可以查阅下列链接: