goroutine是在后台运行的轻量级执行线程,同时它也是Go中实现并发的关键。
goroutine是什么
我们在理论上理解了goroutine
的工作原理,但在代码中,它是什么呢?goroutine
只是一个函数或方法,它在后台与其他goroutine
并发运行。goroutine
特点不是来自于函数或方法,而是取决于我们是如何调用它。
Go提供了一个特殊的关键字go
来创建goroutine
。当我们调用带有go
前缀的函数或方法时,该函数或方法在goroutine
中执行。让我们看一个简单的例子。
https://play.golang.org/p/pIGsToIA2hL
以上程序中,我们创建了一个打印Hello World
的printHello
函数到控制台。在main
函数中,我们像调用普通函数一样调用printHello()
,并得到了预期的结果。
现在让我们通过printHello
函数创建goroutine
。
https://play.golang.org/p/LWXAgDpTcJP
如前所述,根据goroutine
的语法规则,我们在函数调用前加上go
关键字,程序执行得很好。结果如下:
main execution started
main execution stopped
Hello World
没有被打印出来,这有点奇怪。到底发生了什么?
goroutine
总是在后台运行。在这里,当goroutine
被执行时,Go
不会阻塞程序的执行,这与我们在前面的示例中看到的普通函数调用不同。相反,调度器会立即返回到下一行代码,并且忽略goroutine
返回的任何值。但即便如此,为什么我们看不到函数输出呢?
默认情况下,每个Go独立应用程序创建一个goroutine
叫做main goroutine
, main
函数在其上运行。在上面的例子中,在main goroutine
内部的printHello
函数是另一个goroutine
,我们将其称为printHello goroutine
。因此,当我们执行上面的程序时,有两个goroutine
同时运行。正如我们在前面的程序中看到的,goroutines
是协作调度的。因此,当main goroutine
开始执行时,go调度程序不会将控制权交给给printHello goroutine
,直到main goroutine
执行完。但是不幸的是,当main goroutine
执行完毕时,程序立即退出了,调度器没来得及调度到printHello goroutine
上。但是从其他经验中我们知道,使用阻塞条件,我们可以手动调度到其它goroutine
,也就是说显示告诉调度程序调度其他可用的goroutine
。让我们利用time.Sleep()
来做这件事。
https://play.golang.org/p/ujQKjpALlRJ
我们修改了程序,在main goroutine
运行到最后一行代码之前,我们使用time.Sleep(10 * time.Millisecond)
显示调度到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
后退出程序。
https://play.golang.org/p/rWvzS8UeqD6
以上程序仍将打印相同的结果
main execution started
Hello World!
main execution stopped
如果不是1毫秒,而是15毫秒,会发生什么呢?
https://play.golang.org/p/Pc2nP2BtRiP
在这种情况下,main goroutine
将在printHello goroutine醒来之前可用来调度调度程序,这也将在调度程序有时间再次调度printHello goroutine之前立即终止程序。因此,它的产量将低于计划
main execution started
main execution stopped
多个goroutines协同
正如我前面所说,您可以创建尽可能多的goroutine。让我们定义两个简单的函数,一个打印字符串的字符,另一个打印整数片的数字。
https://play.golang.org/p/SJano_g1wTV
在上面的程序中,我们从两个函数调用中创建了两个goroutine。然后我们调度两个goroutine中的任何一个,调度哪个goroutine由调度程序决定。这将产生以下结果
main execution started
H e l l o 1 2 3 4 5
main execution stopped
上述结果再次证明了goroutine是协同调度的。我们再加一次。函数定义中的打印操作之间的休眠调用,以告诉调度程序调度其他可用的goroutine
。
在上面的程序中,我们打印了额外的信息,以查看从程序执行时起打印语句何时执行。理论上,main goroutine
将休眠200毫秒,因此所有其他goroutine
必须在200毫秒内完成它们的工作,然后才会唤醒并终止程序。getChars goroutine
将打印一个字符并休眠10毫秒,调度getDigits goroutine
, getDigits goroutine
将打印一个数字,休眠3毫秒,当getChars goroutine
醒来时,再次调度getChars goroutine
。因为getChars goroutine
可以多次打印和休眠,至少在其他goroutine
休眠时可以打印2次,所以我们希望看到连续打印的字符比数字多。
下面的结果是从运行上述程序在Windows机器。
main execution started at time 0s
H at time 1.0012ms <-|
1 at time 1.0012ms | almost at the same time
e at time 11.0283ms <-|
l at time 21.0289ms | ~10ms apart
l at time 31.0416ms
2 at time 31.0416ms
o at time 42.0336ms
3 at time 61.0461ms <-|
4 at time 91.0647ms |
5 at time 121.0888ms | ~30ms apart
main execution stopped at time 200.3137ms | exiting after 200ms
我们可以看到我们讨论过的模式。一旦您看到程序执行关系图,这将被清除。我们将近似于打印命令需要1ms的CPU时间,与200ms相比,这是微不足道的。
image.png
现在我们了解了如何创建goroutine
以及如何使用它们。但使用时间。睡觉只是为了看看结果。在生产中,我们不知道goroutine
执行需要多少时间。因此,我们不能只是在主函数中添加随机睡眠调用。我们想让goroutines告诉我们他们什么时候执行完毕。同样,在这一点上,我们不知道如何从其他goroutine
获取数据或将数据传递给它们,简单地说,就是与它们通信。这就是频道的作用。让我们在下节课讨论它们。
匿名goroutines
如果匿名函数可以存在,那么匿名goroutine
也可以退出。请立即从函数课中阅读已调用函数以理解本节。让我们修改前面的printHello goroutine
示例。
https://play.golang.org/p/KSzsPIuG-Ph
结果非常明显,因为我们在同一语句中定义了函数并将其作为goroutine
执行。
所有goroutine
都是匿名的,因为goroutine
没有标识。但我们调用它的意义是,创建它的函数是匿名的。
所有goroutine都是匿名的,因为goroutine没有标识。但我们调用它的意义是,创建它的函数是匿名的。
翻译原文
网友评论