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
}
}()
}