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 协程中运行。

举个简单的栗子:

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

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

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

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

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

main execution started
main execution stopped

奇怪的是,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() 函数去实现它吧。

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

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

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

main execution started
Hello World!
main execution stopped

下面我稍微修改一下例子,我在 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 ”并且结束整个程序。

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

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

main execution started
Hello World!
main execution stopped

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

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

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

main execution started
main execution stopped

使用多 Go 协程

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

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

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

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

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

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

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

我们在 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 来说是可以忽略不计的。

现在我们已经知道了如何去创建 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:深度剖析 Go 中的 Go 协程 (goroutines) -- Go 的并发

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

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

go test 如何输出到控制台_深度剖析 Go 中的 Go 协程 (goroutines) -- Go 的并发相关推荐

  1. string类有可以调换方向的函数吗_深度剖析C++中的inline函数

    点蓝色字关注"CurryCoder" 微信公众号:CurryCoder的程序人生 怕什么真理无穷,进一寸有一寸的欢喜 1.inline函数的爱恨两难 内联函数比宏优点好很多,详细原 ...

  2. 深度卷积神经网络_深度卷积神经网络中的降采样

    加入极市专业CV交流群,与6000+来自腾讯,华为,百度,北大,清华,中科院等名企名校视觉开发者互动交流!更有机会与李开复老师等大牛群内互动! 同时提供每月大咖直播分享.真实项目需求对接.干货资讯汇总 ...

  3. tensorflow中同时两个损失函数_深度度量学习中的损失函数

    度量学习(metric learning)研究如何在一个特定的任务上学习一个距离函数,使得该距离函数能够帮助基于近邻的算法(kNN.k-means等)取得较好的性能.深度度量学习(deep metri ...

  4. python输出文本框_在文本框中显示打印输出

    我在tkinter中创建了一个GUI,当我单击Get时,它必须生成输出结果,在我的编码中使用Print语句显示输出结果,一旦我单击,它必须显示在一个文本框中以及同一个GUI中的滚动条. 我的代码:im ...

  5. pcm 降采样_深度卷积神经网络中的降采样

    降采样指的是成比例缩小特征图宽和高的过程,比如从(W,H)变为(W/2,H/2).深度卷积神经网络中降采样的方法主要有三种: 1.stride大于1的pooling 2.stride大于1的conv ...

  6. java gui 控制台_在GUI面板中创建Java控制台

    这是一个功能强大的课程.您可以将此实例安装到系统中并使用以下错误: PrintStream con=new PrintStream(new TextAreaOutputStream(...)); Sy ...

  7. mac命令行将输出写入文件_如何在Linux中使用命令行将PDF文件转换为可编辑文本...

    mac命令行将输出写入文件 There are various reasons why you might want to convert a PDF file to editable text. M ...

  8. arraylist转int数组_深度剖析Java集合之ArrayList

    一. ArrayList 初识 ArrayList是集合的一种实现,实现了接口List,List接口继承了Collection接口. ArrayList 是java 中最常用的集合类型,这是因为它使用 ...

  9. 中计算散度的函数_深度神经网络优化中的不可导函数如何计算梯度?

    众所周知,神经网络仅能处理连续的浮点数,标准的输出也是连续型的数字.但实际问题中,我们很多时候都需要一个离散的结果,比如分类问题中我们希望输出正确的类别,"类别"是离散的,&quo ...

最新文章

  1. 058_JavaScript函数arguments对象
  2. 一千个不用 Null 的理由,你还用?
  3. 51`CTO下载中心——我的新爱
  4. 集合计数 二项式反演_对计数数据使用负二项式
  5. 使用Redis的简单消息队列
  6. linux下enum类型占几个字节,enum大小问题
  7. IDEA配置Docker一键部署SpringBoot项目(企业级做法)
  8. 【React】添加新组件
  9. (5)二进制文件方式部署Kubernetes高可用集群----------创建kubeconfig文件Token.csv随机数文件
  10. 目标跟踪 MOSSE(Visual Object Tracking using Adaptive Correlation Filters)
  11. 模拟器:思科 创建Vlan,给2层交换机和3层交换机配置IP地址和子网掩码
  12. php界面怎么美化,美化你的应用程序的外观界面
  13. 英雄联盟显示计算机内存不足怎么办,玩英雄联盟内存不足的解决方法
  14. 【MATLAB】三维绘图 三维数据插值
  15. 《富爸爸穷爸爸》精髓:穷人思维和富人思维的区别
  16. 【NLP】第16章 Transformer驱动副驾驶的出现
  17. 2022中国开发者影响力年度榜单揭晓,华为、阿里、腾讯等入选年度开源贡献企业 | 美通社头条...
  18. antd源码-spin解析
  19. python面向对象编程的思想0727
  20. C++保留两位小数的四种方法

热门文章

  1. JavaScript事件代理和委托
  2. Binary String Matching
  3. Linux下访问光盘数据
  4. iOS buttonWithType:101 苹果私有api
  5. awk:split()函数、数组、自定义函数
  6. Eclipse Rcp系列 http://www.blogjava.net/dreamstone/archive/2007/02/08/98706.html
  7. FineReport 11.0 全新大屏模式,打开3D视界,大屏制作更快
  8. 数字化转型时代,企业管理者应该如何培养数据化管理思维?
  9. 只有单杀技能的飞鸽传书
  10. 飞秋(FeiQ)海量的用户基数决定了这一模式