2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > GCTT 出品 | 深度剖析 Go 中的 Go 协程 (goroutines) -- Go 的并发

GCTT 出品 | 深度剖析 Go 中的 Go 协程 (goroutines) -- Go 的并发

时间:2021-09-27 15:15:49

相关推荐

GCTT 出品 | 深度剖析 Go 中的 Go 协程 (goroutines) -- Go 的并发

Go 协程 (goroutine) 是指在后台中运行的轻量级执行线程,go 协程是 Go 中实现并发的关键组成部分。

在上次的课程中,我们学习了Go的并发模型。由于Go协程相对于传统操作系统中的线程 (thread) 是非常轻量级的,因此对于一个典型的Go应用来说,有数以千计的Go协程并发运行的情形是十分常见的。并发可以显着地提升应用的运行速度,并且可以帮助我们编写关注点分离(Separation of concerns,Soc)的代码。

什么是 Go 协程?

我们也许在理论上已经知晓Go协程是如何工作的,但是在代码层级上,go协程何许物也?其实,go协程看起来只是一个与其他众Go协程并发运行的一个简单函数或者方法,但是我们并不能想当然地从函数或者方法中的定义来确定一个Go协程,go协程的确定还是要取决于我们如何去调用。

Go中提供了一个关键字go来让我们创建一个Go协程,当我们在函数或方法的调用之前添加一个关键字go,这样我们就开启了一个Go协程,该函数或者方法就会在这个Go协程中运行。

举个简单

/p/pIGsToIA2hL

在上面的代码中,我们定义了一个可以在控制台输出Hello World字符串的printHello的函数,在main函数中,我们就像平时那样调用printHello函数,最终也是理所当然地获得了期望的结果。

下面,让我们尝试从同一个函数创建Go协程:

/p/LWXAgDpTcJP

根据Go协程的语法,我们在函数调用的前面增加了一个go关键字,之后程序运行正常,输出了以下的结果:

mainexecutionstartedmainexecutionstopped

奇怪的是,Hello World并没有如同我们预料的那样输出,这期间究竟发生了什么?

go协程总是在后台运行,当一个Go协程执行的时候(在这个例子中是go printHello()),Go并不会像在之前的那个例子中在执行printHello中的功能时阻塞main函数中剩下语句的执行,而是直接忽略了Go协程的返回并继续执行main函数剩下的语句。即便如此,我们为什么没法看到函数的输出呢?

在默认情况下,每个独立的Go应用运行时就创建了一个Go协程,其main函数就在这个Go协程中运行,这个Go协程就被称为 go 主协程(main Goroutine,下面简称主协程)。在上面的例子中,主协程 中又产生了一个printHello这个函数的Go协程,我们暂且叫它printHello协程 吧,因而我们在执行上面的程序的时候,就会存在两个Go协程(mainprintHello)同时运行。正如同以前的程序那样,go协程们会进行协同调度。因此,当 主协程 运行的时候,Go调度器在 主协程 执行完之前并不会将控制权移交给printHello协程。不幸的是,一旦 主协程 执行完毕,整个程序会立即终止,调度器再也没有时间留给printHello协程 去运行了。

但正如我们从其他课程所知,通过阻塞条件,我们可以手动将控制权转移给其他的Go协程 , 也可以说是告诉调度器让它去调度其他可用空闲的 Go 协程。让我们调用time.Sleep()函数去实现它吧。

/p/ujQKjpALlRJ

如上图所示,我们修改了程序,程序在main函数的最后一条语句之前调用了time.Sleep(10 * time.Millisecond),使得 主协程 在执行最后一条指令之前调度器就将控制权转移给了printhello协程。在这个例子中,我们通过调用time.Sleep(10 * time.Millisecond)强行让 主协程 休眠10ms并且在在这个10ms内不会再被调度器重新调度运行。

一旦printHello协程 执行,它就会向控制台打印‘Hello World!’,然后该Go协程(printHello协程)就会随之终止,接下来 主协程 就会被重新调度(因为main Go协程已经睡够10ms了),并执行最后一条语句。因此,运行上面的程序就会得到以下的输出 :

mainexecutionstartedHelloWorld!mainexecutionstopped

下面我稍微修改一下例子,我在printHello函数的输出语句之前添加了一条time.Sleep(time.Millisecond)。我们已经知道了如果我们在函数中调用了休眠(sleep)函数,这个函数就会告诉Go调度器去调度其他可被调度的Go协程。在上一课中提到,只有非休眠(non-sleeping)的Go协程才会被认为是可被调度的,所以主协程在这休眠的 10ms 内是不会被再次调度的。因此 主协程 先打印出“main execution started” 接着就创建了一个printHello协程,需要注意此时的 主协程 还是非休眠状态的,在这之后主协程就要调用休眠函数去睡 10ms,并且把这个控制权让出来给printHello协程。printHello协程会先休眠1ms告诉调度器看看有没有其他可调度的Go协程,在这个例子里显然没有其他可调度的Go协程了,所以在printHello协程结束了这1ms的休眠户就会被调度器调度,接着就输出了“Hello World”字符串,之后这个Go协程运行结束。之后,主协程会在之后的几毫秒被唤醒,紧接着就会输出“main execution stopped”并且结束整个程序。

/p/rWvzS8UeqD6

上面的程序依旧和之前的例子一样,输出以下相同的结果:

mainexecutionstartedHelloWorld!mainexecutionstopped

要是,我把这个printHello协程中的休眠1毫秒改成休眠15毫秒,这个结果又是如何呢?

/p/Pc2nP2BtRiP

在这个例子中,与其他的例子最大的区别就是printHello协程比主协程的休眠时间还要长,很明显,主协程要比printHello协程唤醒要早,这样的结果就是主协程即使唤醒后执行完所有的语句,printHello协程还是在休眠状态。之前提到过,主协程比较特殊,如果主协程执行结束后整个程序就要退出,所以printHello协程得不到机会去执行下面的输出的语句了,所以以上的程序的数据结果如下:

mainexecutionstartedmainexecutionstopped

使用多 Go 协程

就像之前我所提到过的,你可以随心所欲地创建多个Go协程。下面让我们定义两个简单的函数,一个是用于顺序打印某个字符串中的每个字符,另一个是顺序打印出某个整数切片中的每个数字。

/p/SJano_g1wTV

在上图中的程序中,我们连续地创建了两个Go协程,程序输出的结果如下:

mainexecutionstartedHello12345mainexecutionstopped

上面的结果证实了Go协程是以合作式调度来运作的。下面我们在每个函数中的输出语句的下面添加一行time.Sleep,让函数在输出每个字符或数字后休息一段时间,好让调度器调度其他可用的Go协程。

/p/lrSIEdNxSaH

在上面的程序中,我又修改了一下输出语句使得我们可以看到每个字符或数字的输出时刻。理论上主协程会休眠200ms,因此其他Go协程要赶在主协程唤醒之前做完自己的工作,因为主协程唤醒之后就会导致程序退出。getChars协程每打印一个字符就会休眠10ms,之后控制权就会传给getDigits协程,getDigits协程每打印一个数字后就休眠30ms,若getChars协程唤醒,则会把控制权传回getChars协程,如此往复。在代码中可以看到,getChars协程会在其他协程休眠的时候多次进行打印字符以及休眠操作,所以我们预计可以看到输出的字符比数字更具有连续性。

我们在Windows上运行上面的程序,得到了以下的结果:

mainexecutionstartedattime0sHattime1.0012ms<-|1attime1.0012ms|almostatthesametimeeattime11.0283ms<-|lattime21.0289ms|~10msapartlattime31.0416ms2attime31.0416msoattime42.0336ms3attime61.0461ms<-|4attime91.0647ms|5attime121.0888ms|~30msapartmainexecutionstoppedattime200.3137ms|exitingafter200ms

通过以上输出结果可以证明我们之前对输出的讨论。对于这个结果,我们可以通过下面的的程序运行图来解释。需要注意的是,我们在图中定义一个输出语句大约会花费1msCPU时间,而这个时间相对于200ms来说是可以忽略不计的。

现在我们已经知道了如何去创建Go协程以及去如何去使用它。但是使用time.Sleep只是一个让我们获取理想结果的一个小技巧。在实际生产环境中,我们无法知晓一个Go协程到底需要执行多长的时间,因而在main函数里面添加一个time.Sleep并不是一个解决问题的方法。我们希望Go协程在执行完毕后告知主协程运行的结果。在目前阶段,我们还不知道如何向其他Go协程传递以及获取数据,简而言之,就是与其他 Go 协程进行通信。这就是channels引入的原因。我们会在下一次课中讨论这个东西。

匿名 Go 协程

如果一个匿名函数可以退出,那么匿名Go协程也同样可以退出。请参照functions课程中的 即时调用函数(Immedietly invoked function) 来理解本节。让我们修改一下之前printHello协程的例子:

结果非常明显,因为我们定义了匿名函数,并在同一语句中作为Go协程执行。

需要注意的是,所有的Go协程都是匿名的,因为我们从并发(concurrency一课中学到,go协程是不存在标识符的,在这里所谓的匿名Go协程只是通过匿名函数来创建的Go协程罢了

via: /rungo/anatomy-of-goroutines-in-go-concurrency-in-go-a4cb9272ff88

作者:Uday Hiwarale 译者:hafrans 校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。