1. 先说说go的地位
连续五年,Rust 一直是最受欢迎的编程语言。与去年相比,TypeScript 是第二个超过 Python 的代码。go大幅上升,从去年的第10位上升到第5位。
Go 以其对并发性的一流支持而闻名,或者说是一个程序同时处理多个事情的能力。并发运行代码正在成为编程中更为关键的一部分。 通过将程序设计为并发运行来使程序运行得更快,这样程序的每个部分都可以独立于其他部分运行。Go 中的三个特性 goroutines、channel和select 使并发在组合在一起时更加容易。
Goroutines 解决了在程序中运行并发代码的问题,而通道解决了并发代码之间的安全通信问题。goroutines是go最好的特色之一!它们非常轻量级,不像操作系统线程,而是数百个 Goroutines 可以被多路复用到一个操作系统线程上(Go 有它的运行时调度器) ,只需要最小的上下文切换开销!简单地说,goroutines 是线程上的一种轻量级和廉价的抽象。
2 运行时调度器 Go Runtime Scheduler
Scheduler 的工作是将可运行的 goroutine (G)分布在运行在一个或多个处理器(P)上的多个工作 OS 线程(M)上。处理器处理多个线程。线程正在处理多个 goroutine。处理器P的数量取决于 CPU 核的数量。
3 Goroutine 是什么?
Goroutines 是与其他函数并发运行的函数。Goroutines 可以被认为是位于操作系统线程之上的轻量级线程。与线程相比,创建 Goroutine 的成本很小。因此,Go 应用程序通常会有数千个 Goroutines 并发运行。Goroutines 被多路复用到较少数量的操作系统线程上。一个程序中可能只有一个线程具有数千个 goroutine。所有这些都由 Go 的运行时调度程序负责。Goroutine 有三种状态: 运行状态、可运行状态和不可运行状态。
线程 VS Goroutines
为什么不使用简单的操作系统线程呢?Goroutines 已经在操作系统线程之上运行了。但区别在于,多个 Goroutines 运行在单个 OS 线程上。创建 goroutine 不需要太多内存,只需要2kB 的堆栈空间。它们通过根据需要分配和释放堆存储来增长。相比之下,线程开始于一个更大的空间,以及一个称为保护页的内存区域,该区域充当一个线程的内存。Goroutines 很容易在运行时创建和销毁,但线程有很大的设置和清理成本; 它必须从操作系统请求资源。
运行时被分配了的个线程M,所有 goroutine 都在这些线程上复用。在任何时候,每个线程都将执行一个 goroutine。如果 goroutine 被阻塞(函数调用、系统调用、网络调用等) ,将会被切换到goroutine,该 goroutine 将在该线程M上执行。总之,Go 使用的是 Goroutines 和 Threads,它们在并发执行函数的组合中都是必不可少的。但是 Go 正在使用 Goroutines,这使得 Go 成为一种比最初看起来更伟大的编程语言。
Goroutines 队列
Go 在本地队列和全局队列两个级别上管理 goroutine。本地队列附加到每个处理器P,而全局队列是常见的。Goroutines 不会只在本地队列已满时才进入全局队列,当 Go 向调度程序注入一系列 Goroutines 时,它们也会被推入全局队列,例如,从网络轮询程序;
Stealing Work 偷窃
当一个处理器P没有任何 Goroutines 时,它按照以下顺序提取G:
- 从本地队列中提取工作
- 从网络轮询器中提取工作
- 从另一个处理器的本地队列中提取工作
- 从全局队列中提取工作
所以G可以在不同的P上执行;
上图中,可以看到 P1用完了 goroutine。Go 的运行时调度器将检查 netpoller 中已完成的 IO 请求(syscalls、网络请求)。如果这个 netpoller 为空,将会从其他处理器获取 goroutines。如果其他每个处理器P运行队列为空,处理器将尝试从全局运行队列中获取 goroutines。