[Go 入门] 第十章 使用 Goroutine
Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除
Goroutine 是应对网络延迟的方式之一, 本文内容:
理解并发
要理解 Goroutine,必须要明白你并发的含义。在最简单的计算机程序中,操作是依次执行的,执行顺序与出现顺序相同。
这种行为像餐馆服务员给顾客点菜。服务员必须先将菜单给顾客,然后顾客看过菜单并点菜完成后,服务员才能将点菜单交给厨师,服务员给顾客提供服务的过程大致如下:
- 将菜单递给顾客
- 接受顾客的点菜单
- 将点菜单交给厨师
- 从厨师那里取菜
- 将菜交给顾客
为这个过程编写程序时,完全可以认为一个任务完成后才能接着执行下一个任务。这个过程很像按出现顺序执行脚本中的代码——一行代码执行完再执行下一行
对于很多编程任务而言,按顺序执行任务的理念不仅可行,而且效果很显著。下面是一些这样的例子:
- 基于伦次的简单终端游戏
- 温度转换器
- 随机数生成器
另一种理念是不必等到一个操作执行完毕后再执行下一个,编程任务和编程环境越复杂,这种理念就越重要。提出这种理念旨在让程序能够应对更复杂的情形,避免执行完一行代码后再执行下一行,从而提高程序的执行速度。程序完全按顺序执行时,如果某行代码需要很长时间才能执行完毕,那么整个程序将可能因此而停止,导致用户长时间等待时间的发生。
现代编程必须考虑众多时间不可预测的变数。例如,您无法确定网络调用需要多长时间才能完成,也无法确定读取磁盘文件需要多长时间
假设程序需要从天气服务那里获取某个地方当前的天气情况是,就需要编写一些代码来执行这种请求并处理 Web 服务器的响应。程序发出请求后,很多因素都可能影响响应返回的速度,如以下几种
- 查找天气服务地址的DNS的速度
- 程序和天气服务器之间的网络连接速度
- 建立与天气服务器连接的速度
- 天气服务器的响应速度
鉴于所有这些因素都不是发出请求的程序能够控制的,因此完全有理由认为响应速度是无法预测的。另外,每次请求得到相应的时间都可能不同。面对这样的情形,程序员可选择等待相应——阻塞程序直到响应返回为止,也可继续执行其他有用的任务,大多数现代编程语言都提供了选择空间,让程序员可等待响应,也可继续做其他事情
回头来看餐厅服务员给顾客提供服务的过程,完成其中每个步骤的时间都是不确定的
- 顾客需要多长时间才能入座
- 顾客需要多长时间才能点好菜
- 顾客需要多长时间才能下单
- 顾客需要多长时间才能开始做菜
- 顾客需要多长时间才能将菜做好
如果按顺序做,服务员能够很好地为顾客服务,但无法同时为其他顾客提供服务!如果每位服务员都专为一位顾客服务,则餐厅的收费将非常高,相反,服务员可以并发地执行任务。这意味着在厨师做菜时,服务员可以让其他顾客点菜,并在其他顾客点菜期间去取菜
在现实世界中,有很多事情是可以同时进行的:乘客在上车的同时听音乐,在排队的时候读书,鉴于此,编程语言有必要提供模拟这种情形的方式
随着互联网的日益普及,网络编程越来越常见:程序可能想多个服务器请求信息;数据库可能位于另一个完全不同的网络中。鉴于一切的都是基于网络的,要可靠的预测任务完成的时间是很难做到的。
并发和并行
理解并发后,该讨论 Goroutine 了,但在此之前先来说说并发和并行的差别。这一点很重要,我们以生日聚会烘焙100个蛋挞为例,来说明并发和并行的差别。我们假设蛋糕粉和烘焙托盘多得不得了,而我们的目标是尽快把蛋挞烤好。
如果采用顺序的方式:就意味着每次只能在一个烤箱中烤一个蛋挞。这种做法的效率显然很低,因为这需要很长时间:烤好一个蛋挞后再将另一个蛋挞放入烤箱,另外,时间上也无法预测,因为有的蛋挞烤得快,有的烤的慢
一种并发方式是,使用烘烤托盘每次烤多个蛋挞。这样做的效率要搞很多,但还是不能同时烤好所有的蛋挞。例如,根据蛋挞的大小和所处烤箱的位置,烤好每个蛋挞的时间可能不同。相比于顺序方法,这种做法的速度更快,快多少取决于可同时烘烤多少个蛋挞。
并发方法的速度受制于众多因素,其中一个是烤箱的尺寸。如果有位朋友家也有烤箱,就可以两家同时烤,从而进一步提高效率。同时烤多个蛋挞被称为并发,而将烤蛋挞的任务分为两部分,由两家分别烤,烤好以后再放在一起,这称为并行。以并行的方式执行任务时,可利用并发性,也可不利用;它相当于将工作分成多个部分,各部分的工作完成后再将结果合并。
二两的差别就在于:
并发就是同时处理很多事情,而并行就是同时做很多事情
在现代编程中,并发是不可或缺的部分。对有些程序来说,并发至关重要(在确保性能方面尤其如此)例如:
- 聊天程序
- 多玩家游戏
- Web服务器
- 从磁盘读取数据
Google日常工作的并发需求是催生出 Go 的动力之一,因为使用传统的系统语言难以编写高效的并发代码
通过 Web 浏览器来理解并发
大家每天都要使用 Web 浏览器,而网站的加载速度通常很快,这都是拜并发性所赐。为何成网页将其显示给用户,浏览器背后的技术必须做大量的并发工作。通常,网页由图像和脚本组成,而这些图像和脚本来自网上众多不同的服务器。
在浏览器的开发者工具中,在地址栏输入网址后回车,可以观察到发送的请求,这里需要所述的就是,浏览器并不依次发送请求,而是同时发送请求,以尽快渲染页面或其组成部分。这样做的结果就是,页面的加载速度在用户看起来会很快。仅当所有请求都结束后,页面才加载完毕,但在此之前浏览器依然能够做很多有用的事情。
阻塞和非阻塞代码
基于对并发性的大致认识,下面来编写一个程序,模拟函数调用阻塞程序的执行直到操作完成的情形。为模拟缓慢的函数调用,可使用 time.Sleep,它的作用是让程序暂停指定的时间。在实际编程中,这可能是缓慢的函数调用或需要运行很长时间的函数
1 |
|
解读如下:
- 这个程序执行时,将调用执行方法 time.Sleep 的函数 slowFunc
- 方法 time.Sleep 让程序暂停 2s
- 程序暂停时不会执行其他代码,因此暂停期间不会执行函数 main 中的第二行代码
- 2s 后,函数 slowFunc 打印一行文本再返回
- 控制权返回到 main 函数,因此执行第二行代码 —— 向终端打印一条消息
这些代码是阻塞的,因为等待函数 slowFunc() 返回期间,没有执行其他代码
使用 Goroutine 处理并发操作
Go 语言提供了 Goroutine,可在调用函数 slowFunc 后立即执行 main 函数中的第二行代码。这种情况下函数 slowFunc 依然会执行,但不会阻塞程序中的其他代码行的执行
Goroutine 使用非常简单,只需要在 Goroutine 执行的函数或者方法前加上关键字 go 即可。
1 |
|
执行后,不能看到调用 slowFunc 的结果,这是因为 Goroutine 立即返回。这意味着程序将接着执行后面的代码,然后退出。如果没有其他因素阻止,则程序将在 Goroutine 返回前就退出。
下面代码将演示,如何利用 Goroutine 来实现并发
1 |
|
执行后的输出则符合预期:1
2
3I am now show straightaway !
sleeper() finished
定义 Goroutine
并发是编程语言中提供的一种常见功能。例如,Node.JS 使用时间循环来管理并发,而 Java 使用线程。 Apache 和 Nginx 等 Web 服务器也使用不同的并发方法,Apache 喜欢使用线程和进程,而 Nginx 使用事件循环。
与 Java 一样,Go在幕后使用线程来管理并发,但 Goroutine 让程序员无需直接管理线程。创建一个 Goroutine 只需占用几KB的内存,因此创建数千个 Goroutine 也不会耗尽内存,另外,创建和销毁 Goroutine 的效率也非常高。
Goroutine 是一个并发抽象。因此开发人员通常无需准确地知道操作系统中发生的情况。
问题列表
Goroutine 为何要立即返回
Goroutine 之所以立即返回,是因为这样才符合非阻塞执行的理念。
可在那些情况下使用 Goroutine
在事件发生顺序未知的情况下,使用 Goroutine 是不错的选择。比如网络调用,读取磁盘文件以及创建事件驱动的程序(如聊天应用和游戏)
All articles in this blog adopt the CC BY-SA 4.0 agreement unless otherwise stated. Please indicate the source for reprinting!