1. goroutine 基础知识
1.1 进程
进程(process) 是一个程序的实例,具有某些专用资源,如内存空间、处理器时间、文件句柄(例如,Linux 中的大多数进程都有 stdin、stdout 和 stderr) 和至少一个线程。我们称其为实例(instance),这是因为同一个进程可以用来创建多个进程。
在大多数通用操作系统中,每个进程都与其他进程隔离,因此任何两个希望通信实用程序来完成。
当进程终止时,为该进程分配的所有内存都将被释放,所有打开的文件都将被关闭,并且所有线程都将被终止。
1.2 线程
线程( thread ) 是一个执行上下文,包含运行指令序列所需的所有资源。通常,它包含堆栈和处理器寄存器的值。堆栈对于保持该线程内嵌套函数调用的顺序以及存储在该线程中执行的函数中声明的值是必需的。一个给定的函数可能在许多不同的线程中执行,因此该函数在线程中运行时使用的局部变量将存储在该线程的堆栈中。
1.3 调度程序
调度程序 (scheduler) 可以将处理器时间分配给线程。有些调度程序是抢占式的,可以随时停止一个线程以切换到另一个线程。有些调度程序是协作的,必须等待线程才能切换到另一个线程。线程通常由操作系统管理。
1.4 goroutine
goroutine 是由 Go 运行时管理的执行上下文(而不是由操作系统管理的线程)。goroutine 的启动开销通常比操作系统线程小得多。
goroutine 从一个小堆栈开始,并根据需要进行增长。创建新的 goroutine 比创建操作系统线程更快、更便宜。Go 调度程序将分配操作系统线程来运行 goroutine。
在 Go 程序中, goroutine 是使用 go 关键字创建的,后跟函数调用:
-
go f()
-
go g(i,j)
-
go func(){
-
...
-
}()
-
go func(i,j int) {
-
...
-
}(1,2)
go 关键字在新的 goroutiine 中启动给定的函数。现有的 goroutine 继续与新创建的 goroutine 并发运行。
作为 goroutine 运行的函数可以接收参数,但不能返回值。goroutine 函数的参数在 goroutine 启动之前进行评估,并在 goroutine 开始运行时传递给函数。
你可能会问,为什么需要开发一个全新的线程系统,只是为了获得轻量级线程?
goroutine 不仅仅是轻量级线程。它们是通过在准备运行的 goroutine 之间有效共享处理能力来提高吞吐量的关键。这是该思想的要点。
Go 运行时使用的操作系统线程数等于平台上的处理器/内核数(除非你通过设置 GOMAXPROCS 环境变量或调用 函数来更改此设置)。这是平台可以并行执行的操作数量。除些之外,操作系统将不得不求助于分时系统。
由于 GOMAXPROCS 线程并行运行,因此操作系统级别没有上下文切换开销。Go 调度程序将 goroutine 分配给操作系统线程,以便在每个线程上完成更多工作,而不是在许多线程上完成更少的工作。
较小的上下文切换并不是 Go 调度程序比操作系统调度程序性能更好的唯一原因。 Go 调度程序之所以表现更好,是因为它知道唤醒哪些 goroutine 以充分利用它们。操作系统不知道通道操作或互斥体,这两种操作都是由 Go 运行时在用户空间中管理的。
1.5 线程和 goroutine 之间的区别
除了 goroutine 更为轻量级,线程和 goroutine 之间还有一些更细微的区别。
线程通常具有优先级。当优先级线程与高优先级线程竞争共享资源时,高优先级线程有更好的机会获得共享资源。
goroutine 没有预先分配的优先级。也就是说,该语言规范允许有一个有利于某些 goroutine 的调度程序。例始, Go 运行时的更高版本包括将选择饥饿 goroutine 的调度算法。
不过,一般来说,正确的并发 Go 程序不应该依赖于调度行为。许多语言都具有诸如使用可配置调度算法的线程池之类的功能。这些功能是基于 “线程创建是一项昂贵的操作” 这一假设而开发的,而 Go 的情况并非如此。
另一个区别是 goroutine 堆栈的管理方式。一个 goroutine 以一个小堆栈开始(1.19 之后的 Go 运行时使用历史平均值,早期版本使用 2KB),每个函数调用都会检查剩余的堆栈空间是否足够。如果不足,则调整堆栈大小。
反观操作系统线程则通常以一个大得多的堆栈(以兆字节为单位)开始,并且该堆栈通常不会增长。