goroutine
goroutine 是 Go 的并发模型的核心概念。为了理解 goroutine,我们来定义几个术语。第一个是进程。进程是程序的实例,由计算机的操作系统运行。操作系统将一些资源(如内存)与进程相关联,并确保其他进程不能访问它们。进程由一个或多个线程组成。一个线程是一个执行单元,由操作系统运行。一个进程中的线程共享对资源的访问。一个 CPU 可以同时执行多少个线程的指令取决于内核的数量。操作系统的工作之一是在 CPU 上调度线程,以确保每个进程(以及进程中的每个线程)都有机会运行。
goroutine 是由 Go 运行时管理的轻量级进程。当 Go 程序启动时,Go 运行时会创建一些线程并启动一个 goroutine 来运行程序。你的程序创建的所有 goroutine(包括程序入口部分)都由 Go 运行时调度器自动分配给这些线程,就像操作系统在 CPU 内核间调度线程一样。这似乎看起来是额外的工作,因为底层操作系统已经包含了一个管理线程和进程的调度器,但 goroutine 有几个好处:
1. 创建 goroutine 比创建线程更快,因为你不是在创建操作系统级的资源。
2. goroutine 的初始栈比线程栈更小,并且可以根据需要增长。这使得 goroutine 的内存效率更高。
3. 在 goroutine 之间切换比在线程之间切换更快,因为 goroutine 之间的切换完全发生在进程内部,避免了操作系统(相对)缓慢的调用。
4. 调度器作为 Go 进程的一部分能够进行优化。当调度器与网络轮询器一起工作时,可以检测 goroutine 何时因为 I/O 阻塞而无法调度。它还与垃圾回收器集成,确保工作在所有操作系统线程之间可以较为平均地分配给 Go 进程。
这些优势使得 Go 程序可以同时生成数百、数千甚至数万个 goroutine。如果你尝试在一种使用本地线程的语言中启动成千上万个线程,程序就会慢到如同乌龟在爬行。
如果你有兴趣了解关于调度器的更多知识,可以听一下 Kavya Joshi在GopherCon 2018上发表的名为“The Scheduler Saga”的演讲(https://oreil.ly/879mk)。
在一个函数调用前放置 go 关键字可以启动一个 goroutine。与其他函数一样,我们可以向它传递参数以初始化其状态。不过,任何函数返回的值都会被忽略。
任何函数都可以作为 goroutine 启动。这与 JavaScript 不同,在 JavaScript 中,只有当使用 async 关键字声明函数时,函数才会异步运行。然而,在 Go 中,大家习惯于用一个封装业务逻辑的闭包来启动 goroutine。该闭包负责管理并发的数据和状态。例如,闭包从通道中读取数值并将其传递给业务逻辑,业务逻辑完全不知道它是在一个 goroutine 中运行的。然后,函数的结果被写回另一个通道。这种职责分离使代码模块化、可测试,并使 API 调用简单,无须关注并发问题:
func process(val int) int {
// do something with val
}
func runTingConcurrently(in <-chan int, out chan<- int) {
go func() {
for val := range in {
result := process(val)
out <- result
}
}()
}