goroutine剖析

2019-08-01  本文已影响0人  wu_sphinx

goroutine是在后台运行的轻量级执行线程,同时它也是Go中实现并发的关键。

goroutine是什么

我们在理论上理解了goroutine的工作原理,但在代码中,它是什么呢?goroutine只是一个函数或方法,它在后台与其他goroutine并发运行。goroutine特点不是来自于函数或方法,而是取决于我们是如何调用它。

Go提供了一个特殊的关键字go来创建goroutine。当我们调用带有go前缀的函数或方法时,该函数或方法在goroutine中执行。让我们看一个简单的例子。

image.png
https://play.golang.org/p/pIGsToIA2hL

以上程序中,我们创建了一个打印Hello WorldprintHello函数到控制台。在main函数中,我们像调用普通函数一样调用printHello(),并得到了预期的结果。
现在让我们通过printHello函数创建goroutine

image.png
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()来做这件事。

image.png
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后退出程序。

image.png
https://play.golang.org/p/rWvzS8UeqD6

以上程序仍将打印相同的结果

main execution started
Hello World!
main execution stopped

如果不是1毫秒,而是15毫秒,会发生什么呢?

image.png
https://play.golang.org/p/Pc2nP2BtRiP

在这种情况下,main goroutine将在printHello goroutine醒来之前可用来调度调度程序,这也将在调度程序有时间再次调度printHello goroutine之前立即终止程序。因此,它的产量将低于计划

main execution started
main execution stopped

多个goroutines协同

正如我前面所说,您可以创建尽可能多的goroutine。让我们定义两个简单的函数,一个打印字符串的字符,另一个打印整数片的数字。

image.png
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示例。

image.png
https://play.golang.org/p/KSzsPIuG-Ph

结果非常明显,因为我们在同一语句中定义了函数并将其作为goroutine执行。
所有goroutine都是匿名的,因为goroutine没有标识。但我们调用它的意义是,创建它的函数是匿名的。

所有goroutine都是匿名的,因为goroutine没有标识。但我们调用它的意义是,创建它的函数是匿名的。

翻译原文

上一篇下一篇

猜你喜欢

热点阅读