一、Go:它是什么?
除非你一直生活在石头下,否则你可能已经听说过 Golang。Golang 或 Go 是 Google 在21世纪初开发的一种编程语言。其最有趣的特性之一是它通过使用 goroutines 来支持并发,这些 goroutines 就像轻量级的线程。Goroutines 比实际的线程要便宜得多,甚至每秒可以调度数百万的 goroutines。
但 Go 是如何实现这一令人难以置信的壮举的呢?它是如何提供这种能力的?让我们看看 Golang 调度器在背后是如何工作的。
二、前提条件
在我们深入探讨之前,有一些前提条件我必须谈论。如果你已经对它们非常熟悉,可以跳过它们。
1.系统调用
系统调用是向内核的接口。就像 web 服务为用户暴露一个 REST API 接口一样。
系统调用为 Linux 内核提供了一个接口。
为了更多地了解这些系统调用,让我们写一点代码!你可能首先问的问题是,选择哪种语言?
答案有点复杂,但让我们先了解如何在 C 语言中做,然后再花一些时间思考如何在其他语言中执行相同的操作。
现在,让我们尝试 stat 系统调用。该调用非常简单。它接受一个文件路径并返回有关该文件的大量信息。现在,我们将打印返回的几个值,即文件的所有者和文件的大小(以字节为单位)。
#include
#include
int main()
{
//pointer to stat struct
struct stat sfile;
//stat system call
stat("myfile", &sfile); //accessing some data returned from stat
printf("uid = %onfileszie = %lldn", sfile.st_uid, sfile.st_size); return 0;
}
输出是相当容易预测的,如下所示:
uid = 765
filesize = 11
所有的语言在内部都调用相同的系统调用,因为你只能使用这些系统调用与内核进行交互。例如,看一些 NodeJS 代码这里,你可以看到它是如何声明文件路径的。他们围绕这些系统调用做了很多工作,为开发者提供了更简单的接口,但在底层,它们使用内核提供的相同的系统调用。
2.线程
我相信大多数人在生活中某个时候都听说过线程,但你真的知道它们是什么吗?它们是如何工作的?
我会尽快为你解释。
你可能已经读到过有多个核心的处理器。这些核心中的每一个都像一个可以独立工作的独立处理单元。
线程是基于这一点的抽象,我们可以在我们的程序中“创建”线程,并在每个线程上运行代码,然后由内核调度在单个核心上运行。
所以,在任何时候,单个核心都在运行一个执行线程。
我们如何创建线程?通过系统调用!
那为什么不直接使用核心呢?为什么我们不直接写new Core()而是通过new Thread()创建线程?因为在操作系统中可能有多个程序正在运行,每个程序可能都想并行执行代码。由于核心数量有限,每个程序都必须知道有多少可用的核心,如果一个程序占用了所有的核心,它基本上可以完全阻塞操作系统。操作系统可能还需要执行比任何单个程序更重要或优先级更高的工作,所以需要一层抽象。
3.Goroutines
正如我上面解释的,goroutines类似于轻量级的线程,它们可以并发运行并且可以在单独的线程中运行。
在继续之前,没有真正需要了解Golang,但我认为至少在继续之前,看一下一个非常简单的goroutine的实现是有意义的。
package main
import (
"fmt"
"sync"
"time"
)
func printForever(s string) {
// This is an infinite loop that prints the string s forever
for {
fmt.Println(s)
time.Sleep(time.Millisecond)
}
}
func main() {
var wg sync.WaitGroup
// A waitgroup is just a way to wait for goroutines to finish
wg.Add(2)
// Any function executed with the "go" keyword will run as a goroutine
go printForever("HELLO")
go printForever("WORLD")
// This line of code blocks until both goroutines are finished
wg.Wait()
}
输出,你可能猜到了,是连续打印的单词“HELLO”和“WORLD”。
既然用go关键字标记的两个函数调用都是并行运行的。
你的直觉可能会让你认为这些 goroutines 只是并行运行在 CPU 上的单独线程,并由操作系统管理,但这不是真的。尽管现在每次调用都可以是单个线程,但我们很快会发现这并不实际。
三、设计我们自己的调度器
让我们讨论 Go 调度器,就好像我们正在创建自己的调度器一样。我知道这个任务可能看起来很艰巨,但本质上,我们有一些工作单元,比如说一些函数,我们需要将它们调度到有限的线程上。
所以,为了更正式地描述这一点,函数是单一的工作单位。它是执行某些处理的代码行的序列。现在,让我们不要用像 async/await 这样的东西来混淆自己,我们说一个函数只包含非常基本的代码。也许像这样,
int add(int a, int b) {
int sum = a + b;
cout