[奔跑的 Go] 教程十六、深入学习 Go 并发编程之 goroutine

Golang

goroutine 是一个在后台运行的轻量级的执行线程。在 go 中,goroutine 是实现并发的关键。

在前面的课程中,我们了解了 go 的并发模型。相对于系统操作的线程, goroutine 是轻量级的,所以 go 的应用程序通常会并发运行着上千个 goroutine。并发可以显着加速应用程序,并帮助我们编写代码分离(SoC)的代码。

☛ goroutine 是什么?

我们理论上理解了 goroutine 的运行流程,但是在代码中,goroutine 是什么呢?
goroutine 只是一个与其他 goroutine 同时在后台运行的函数或方法。 决定它是否是一个 goroutine 的因素不是函数或方法定义,它取决于我们如何调用它。

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

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

在上面的程序中,我们创建了一个函数 printHello,它将 Hello World! 打印到控制台。 在 main 函数中,我们调用 printHello()就像正常函数调用一样,我们得到了期望的结果。

现在,我们来用 printHello 函数创建一个 goroutine

https://play.golang.org/p/LWXAgDpTcJP

根据 goroutine 的语法,我们使用 go 关键字对函数调用进行了修饰,并且程序正常执行,得到了如下的结果

main execution started
main execution stopped

但是,有点奇怪的是控制台并没有打印出 Hello World。所以在程序运行的时候发生了什么呢?

goroutines 总是运行在后台。 当一个 gorotuine 正在执行的时候,这里的 goroutine 是 go printHello(),go 语言不会阻塞 goroutine 的执行,和前面我看提到的正常函数调用不一样。相反,程序立即返回到下一行代码,并忽略来自goroutine 的任何返回值。但即便如此,为什么我们看不到这个函数的输出?

缺省情况下,每个 go 应用程序都会创建一个 goroutine,main 函数在其中运行,称为 main goroutine。在上述情况下,main goroutine 生成另一个 goroutine 运行 printHello 函数,让我们叫它 printHello goroutine。因此,当我们执行上述程序时,同时运行了两个 goroutine。我们已经了解到,goroutine 是协同调度的。因此,在 main goroutine 执行完之前,printHello goroutine 不会执行。不幸的是,当 main goroutine 执行完毕后,程序会立即结束,调度器没有时间安排 printHello goroutine 执行。但是从之前的课程我们知道,我们可以通过阻塞当前 goroutine,通知调度器运行其他可用的 goroutine,即手动将控制权传递到其他 goroutine。让我们使用 time.Sleep() 来实现这个目的。

https://play.golang.org/p/ujQKjpALlRJ

我们已经修改了程序,在 main goroutine 执行最后一行代码前,通过调用 time.Sleep(10 * time.Millisecond) ,把控制权传递给 printHello goroutine。在这种情况下,main goroutine 将休眠 10ms,在 10ms 内,调度器将不会再次执行它。一旦 printHello goroutine 执行, 它向终端打印 「Hello World!」并终止,然后,main goroutine 继续被调度执行 堆栈指针指向的 最后一行代码(10ms 之后),因此上述程序输出以下结果

main execution started
Hello World!
main execution stopped

当我们在 printHello 函数中增加一个 sleep 调用时,调度器会调度执行另一个可用的 goroutine,在这种情况下是 main goroutine。但是在上一课,我们学到只有 non-sleeping (未休眠) goroutine 会被调度执行,main goroutine 因为在休眠,10ms 内都不会被再次调度。因此 main goroutine 会打印 「main execution started」,生成 printHello goroutine,main goroutine 继续运行, 然后休眠 10ms,把控制权传递给 printHello goroutine。printHello goroutine 休眠 1ms,通知调度器调度其他 goroutine,但是没有其他可供调度的 gotoutine。1ms 后 printHello goroutine 被唤醒,打印 「Hello World!」并退出。几毫秒后 main goroutine 被唤醒,打印「main execution stopped」,并退出程序。

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

上面的程序会打印相同的结果

main execution started
Hello World!
main execution stopped

如果,不是 1ms,我们让 printHello goroutine 休眠 15ms。

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

在这种情况下,main goroutine 将会在 printHello goroutine 之前被唤醒,在调度器有时间调度 printHello goroutine 之前程序就会终止。因此将打印下面的内容

main execution started
main execution stopped

☛ 多协程协同工作

正如我上面所说,你可以创建很多个协程。让我们先定义两个简单的函数,一个来循环打印字符串的每个字符,另一个来循环打印数字切片的每个数字。

https://play.golang.org/p/SJano_g1wTV

在上面的程序中,我们用两个函数连续创建两个协程。接下来我们将开始调度这两个协程,至于哪个协程将被调用取决于调度器。下面是程序执行结果

main execution started
H e l l o 1 2 3 4 5 
main execution stopped

上面的打印结果再次证明了协程是协同调度的。让我们向两个协程函数中分别添加一个time.Sleep来告诉调度器去调度其他可调度的协程。

https://play.golang.org/p/lrSIEdNxSaH

在上面的程序中,我们从程序运行开始打印一些额外的信息去查看print 语句具体的执行时间。理论上,主线程将休息200毫秒,因此其他的协程必须要在这200毫秒内执行完自己的任务,否则主线程将苏醒并退出。getChars协程每10毫秒打印一个字符,然后把控制权交给getDigits协程,getDigits协程每30毫秒打印一个数字直到10毫秒后getChars协程苏醒移交控制权。由于休眠时间长短原因,getDigits协程休眠期间getChars协程至少可打印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

我们可以看到上述图表标注的执行信息清晰的印证了我们上面谈论的执行原理。我们大概估算下print命令将花费CPU一毫秒的时间,相对于200ms的间隔,这个是可以忽略的。

现在我们已经明白了怎样去创建协程并使得它们协同工作。但是调度协程使用time.Sleep仅仅是一种hack(折中)手段。在生产环境中,我们不知道一个协程的运行时间,因此我们不能仅仅在主线程中去添加一个随机休眠时间。我们期望是让协程在执行结束后主动告诉我们。同样的,我们也不能从协程接收数据和发送数据。简单来说就是我们无法和协程进行数据交互操作。而golang的通道就是用来解决上述痛点的。我们将在下篇文章中去详谈通道。

☛ 匿名协程

正如存在匿名函数一样,go中也有匿名协程。详情可以去阅读functions文章的Immedietly invoked function章节来理解这部分。让我们来修改前面的printHellogoroutine.例子。

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

上面的结果非常明显的显示出我们在一个语句中定义函数并让它以协程方式执行。

concurrency 文章中我们学习到所有的协程都是匿名的因为他们没有标志符。但是我们现在是在用一个匿名函数去创建协程,两者是不同的。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/rungo/anatomy-of-goro...

译文地址:https://learnku.com/go/t/31082

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!