goroutine 是一个在后台运行的轻量级执行线程。goroutines 是在 Go 中实现并发的关键。
在前一课(Golang 并发之一 ( go并发模型))中,我们学习了 Go 的并发模型。由于与操作系统线程相比,goroutines 是轻量级的,因此 Go 应用程序同时运行数千个 goroutines 是很常见的。并发可以显着加速应用程序,并帮助我们编写关注点分离 (SoC) 的代码。
什么是goroutine?
我们理解了 goroutine 理论上是如何工作的,但是在代码中,它是什么?其实,goroutine 只是一个与其他 goroutine 在后台同时运行的函数或方法。决定它是否为 goroutine 的不是函数或方法定义,而是由我们如何调用它决定的。
Go 提供了一个特殊的关键字 go
来创建一个 goroutine。当我们调用带有 go
前缀的函数或方法时,该函数或方法在 goroutine 中执行。让我们看一个简单的例子
在上面的程序中,创建了一个函数 printHello
来打印 Hello World!
到控制台。在 main
函数中,我们像普通的函数调用一样调用了 printHello()
,并得到了想要的结果。
现在让我们从同一个 printHello 函数创建 goroutine。
那么,根据 goroutine 语法,我们在函数调用前加上 go 关键字,程序执行得很好。它产生了以下结果:
main execution started
main execution stopped
奇怪的是,"Hello World"没有被打印出来。所以发生了什么事?
goroutines 总是在后台运行。当一个 goroutine 被执行时, 调度器不会阻塞程序的执行,不像我们在前面的例子中看到的普通函数调用。相反,控制会立即执行到下一行代码,并且忽略 goroutine 的任何返回值。但即便如此,为什么我们看不到函数输出呢?
Go规定:每个独立应用程序都必须有一个main函数, main会创建一个goroutine,此是为主协程, 我们暂时称之为main goroutine
。在上面的例子中,main 函数通过 go
关键字调用printHello函数产生另外一个goroutine,我们暂时称之为 printHello goroutine
。因此,当我们执行上述程序时,两个 goroutine 同时运行。正如我们在前面的程序中看到的,goroutine 是同时调度的。
因此,当 main goroutine
执行时,go 调度器不会将控制权传递给 printHello goroutine
,直到 main goroutine
执行完成。不幸的是,当 main goroutine
执行完毕后,程序立即终止了,调度程序没有时间调度 printHello goroutine
。但是正如我们从其他课程(Golang 并发之一 ( go并发模型))中了解到的,使用阻塞条件,我们可以手动将控制权传递给其他 goroutine,也就是告诉调度程序调度其他可用的 goroutine。下面,我们尝试使用 time.Sleep()
调用来做到这一点。
上面我们修改了程序,在 main goroutine
将控制权传递给最后一行代码之前,使用 time.Sleep(10 * time.Millisecond)
阻塞了main进程,将控制权传递给 printHello goroutine
。在这种情况下,main goroutine
会休眠 10 毫秒,并 10 毫秒之内不会在调度到。一旦 printHello goroutine
执行,它会向控制台打印"Hello World!" 并终止,然后 main goroutine
再次被调度(10 毫秒后),并执行最后一行代码。因此,上述程序产生以下结果。
main execution started
Hello World!
main execution stopped
如果我们在函数中添加一个 sleep ,它会阻塞当前goroutine, 并告诉调度器调度另一个可用的 goroutine,在这种情况下是
main goroutine
。但是从上一课中,我们了解到只有非休眠的 goroutine 会被考虑进行调度,main 在休眠的 10 毫秒内不会再次被调度。
下面的例子中,main goroutine
将打印"main execution started",并生成 printHello goroutine
,然后休眠 10 毫秒并将控制权传递给 printHello goroutine
。然后 printHello goroutine
将休眠 1 毫秒,告诉调度程序调度另一个 goroutine,但由于没有可用的 goroutine,1 毫秒后唤醒并打印"Hello World!"然后死亡。然后main goroutine
将在几毫秒后唤醒,打印"main execution stopped"并退出程序。
上面的程序仍然会打印相同的结果
main execution started
Hello World!
main execution stopped
如果不是 1 毫秒,printHello goroutine
休眠 15 毫秒会怎样。
在这种情况下,main goroutine
将在 printHello goroutine
被唤醒之前可用,这也将使程序有时间再次调度 printHello goroutine
之前立即终止。因此它将产生以下输出
main execution started
main execution stopped
使用多个goroutines
正如我之前所说,您可以创建尽可能多的 goroutine。让我们定义两个简单的函数,一个打印字符串的字符,另一个打印整数切片的数字。
在上面的程序中,从两个函数调用中依次创建了2个goroutine,然后调度两个goroutine 中的任何一个,调度哪个 goroutine 由调度程序决定。这将产生以下结果
main execution started
H e l l o 1 2 3 4 5
main execution stopped
我们在上述程序中加入时间线,以更明白的显示调度过程
在上面的程序中,我们打印了额外的信息,以查看自程序执行以来何时执行打印语句。理论上,main goroutine
将休眠 200 毫秒,因此所有其他 goroutine 必须在 200 毫秒内完成工作,然后才能唤醒并杀死程序。 getChars 协程将打印 1 个字符并休眠 10 毫秒,将控制权传递给 getDigits 协程,后者将打印一个数字并休眠 30 毫秒,当它醒来前再次将控制权传递给 getChars 协程。由于 getChars goroutine 可以多次打印和休眠,在其他 goroutine 处于休眠状态时至少 2 次,我们希望看到连续打印的字符多于数字。
main execution started at time 869ns
1 at time 85.786µs |
h at time 74.176µs | 几乎在统一时间执行
e at time 12.69351ms |
l at time 24.539522ms | <- 10ms 的间隔
2 at time 30.357187ms
l at time 36.15972ms
o at time 46.376659ms
3 at time 61.458487ms |
4 at time 95.741154ms |
5 at time 128.897282ms | <- 30ms的间隔
main execution stopped at time 201.411518ms | 200 ms 后执行
通过下面的执行图,我们可以看到我们谈论的模式, 一切都会变的清晰。估计打印命令需要 1 毫秒的 CPU 时间,与 200 毫秒的规模相比,这是可以忽略不计的。
现在了解了如何创建 goroutine 以及如何使用它们。但是使用 time.Sleep 只是为了查看结果。在生产中,我们不知道 goroutine 执行需要多长时间。因此我们不能只在主函数中添加随机睡眠调用。我们希望我们的 goroutine 知道它们何时完成执行。同样在这一点上,我们不知道如何从其他 goroutine 取回数据或将数据传递给它们,简单地与它们通信。这就是channel
的用武之地。让我们在下一课中讨论它们。
匿名 goroutine
如果匿名函数可以存在,那么匿名 goroutine 也可以存在。让我们修改我们之前的 printHello goroutine 示例。
结果非常明显,因为我们在同一语句中定义了函数并作为 goroutine 执行。
正如我们从并发课程中学到的那样,所有 goroutine 都是匿名的,因为 goroutine 没有身份。但我们称其为创建它的函数是匿名的。
参考文章
- 本文翻译