[Go 入门] 第十章 使用 Goroutine

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

Goroutine 是应对网络延迟的方式之一, 本文内容:

理解并发

要理解 Goroutine,必须要明白你并发的含义。在最简单的计算机程序中,操作是依次执行的,执行顺序与出现顺序相同。

这种行为像餐馆服务员给顾客点菜。服务员必须先将菜单给顾客,然后顾客看过菜单并点菜完成后,服务员才能将点菜单交给厨师,服务员给顾客提供服务的过程大致如下:

  1. 将菜单递给顾客
  2. 接受顾客的点菜单
  3. 将点菜单交给厨师
  4. 从厨师那里取菜
  5. 将菜交给顾客

为这个过程编写程序时,完全可以认为一个任务完成后才能接着执行下一个任务。这个过程很像按出现顺序执行脚本中的代码——一行代码执行完再执行下一行

对于很多编程任务而言,按顺序执行任务的理念不仅可行,而且效果很显著。下面是一些这样的例子:

  • 基于伦次的简单终端游戏
  • 温度转换器
  • 随机数生成器

另一种理念是不必等到一个操作执行完毕后再执行下一个,编程任务和编程环境越复杂,这种理念就越重要。提出这种理念旨在让程序能够应对更复杂的情形,避免执行完一行代码后再执行下一行,从而提高程序的执行速度。程序完全按顺序执行时,如果某行代码需要很长时间才能执行完毕,那么整个程序将可能因此而停止,导致用户长时间等待时间的发生。

现代编程必须考虑众多时间不可预测的变数。例如,您无法确定网络调用需要多长时间才能完成,也无法确定读取磁盘文件需要多长时间

假设程序需要从天气服务那里获取某个地方当前的天气情况是,就需要编写一些代码来执行这种请求并处理 Web 服务器的响应。程序发出请求后,很多因素都可能影响响应返回的速度,如以下几种

  • 查找天气服务地址的DNS的速度
  • 程序和天气服务器之间的网络连接速度
  • 建立与天气服务器连接的速度
  • 天气服务器的响应速度

鉴于所有这些因素都不是发出请求的程序能够控制的,因此完全有理由认为响应速度是无法预测的。另外,每次请求得到相应的时间都可能不同。面对这样的情形,程序员可选择等待相应——阻塞程序直到响应返回为止,也可继续执行其他有用的任务,大多数现代编程语言都提供了选择空间,让程序员可等待响应,也可继续做其他事情

回头来看餐厅服务员给顾客提供服务的过程,完成其中每个步骤的时间都是不确定的

  • 顾客需要多长时间才能入座
  • 顾客需要多长时间才能点好菜
  • 顾客需要多长时间才能下单
  • 顾客需要多长时间才能开始做菜
  • 顾客需要多长时间才能将菜做好

如果按顺序做,服务员能够很好地为顾客服务,但无法同时为其他顾客提供服务!如果每位服务员都专为一位顾客服务,则餐厅的收费将非常高,相反,服务员可以并发地执行任务。这意味着在厨师做菜时,服务员可以让其他顾客点菜,并在其他顾客点菜期间去取菜

在现实世界中,有很多事情是可以同时进行的:乘客在上车的同时听音乐,在排队的时候读书,鉴于此,编程语言有必要提供模拟这种情形的方式

随着互联网的日益普及,网络编程越来越常见:程序可能想多个服务器请求信息;数据库可能位于另一个完全不同的网络中。鉴于一切的都是基于网络的,要可靠的预测任务完成的时间是很难做到的。

并发和并行

理解并发后,该讨论 Goroutine 了,但在此之前先来说说并发和并行的差别。这一点很重要,我们以生日聚会烘焙100个蛋挞为例,来说明并发和并行的差别。我们假设蛋糕粉和烘焙托盘多得不得了,而我们的目标是尽快把蛋挞烤好。

如果采用顺序的方式:就意味着每次只能在一个烤箱中烤一个蛋挞。这种做法的效率显然很低,因为这需要很长时间:烤好一个蛋挞后再将另一个蛋挞放入烤箱,另外,时间上也无法预测,因为有的蛋挞烤得快,有的烤的慢

一种并发方式是,使用烘烤托盘每次烤多个蛋挞。这样做的效率要搞很多,但还是不能同时烤好所有的蛋挞。例如,根据蛋挞的大小和所处烤箱的位置,烤好每个蛋挞的时间可能不同。相比于顺序方法,这种做法的速度更快,快多少取决于可同时烘烤多少个蛋挞。

并发方法的速度受制于众多因素,其中一个是烤箱的尺寸。如果有位朋友家也有烤箱,就可以两家同时烤,从而进一步提高效率。同时烤多个蛋挞被称为并发,而将烤蛋挞的任务分为两部分,由两家分别烤,烤好以后再放在一起,这称为并行。以并行的方式执行任务时,可利用并发性,也可不利用;它相当于将工作分成多个部分,各部分的工作完成后再将结果合并。

二两的差别就在于:

并发就是同时处理很多事情,而并行就是同时做很多事情

在现代编程中,并发是不可或缺的部分。对有些程序来说,并发至关重要(在确保性能方面尤其如此)例如:

  • 聊天程序
  • 多玩家游戏
  • Web服务器
  • 从磁盘读取数据

Google日常工作的并发需求是催生出 Go 的动力之一,因为使用传统的系统语言难以编写高效的并发代码

通过 Web 浏览器来理解并发

大家每天都要使用 Web 浏览器,而网站的加载速度通常很快,这都是拜并发性所赐。为何成网页将其显示给用户,浏览器背后的技术必须做大量的并发工作。通常,网页由图像和脚本组成,而这些图像和脚本来自网上众多不同的服务器。

在浏览器的开发者工具中,在地址栏输入网址后回车,可以观察到发送的请求,这里需要所述的就是,浏览器并不依次发送请求,而是同时发送请求,以尽快渲染页面或其组成部分。这样做的结果就是,页面的加载速度在用户看起来会很快。仅当所有请求都结束后,页面才加载完毕,但在此之前浏览器依然能够做很多有用的事情。

阻塞和非阻塞代码

基于对并发性的大致认识,下面来编写一个程序,模拟函数调用阻塞程序的执行直到操作完成的情形。为模拟缓慢的函数调用,可使用 time.Sleep,它的作用是让程序暂停指定的时间。在实际编程中,这可能是缓慢的函数调用或需要运行很长时间的函数

1
2
3
4
5
6
7
8
9
func slowFunc() {
time.Sleep(time.Second * 2)
fmt.Println("sleeper() finished")
}

func main() {
slowFunc()
fmt.Println("I am not shown until slowFunc() completes")
}

解读如下:

  • 这个程序执行时,将调用执行方法 time.Sleep 的函数 slowFunc
  • 方法 time.Sleep 让程序暂停 2s
  • 程序暂停时不会执行其他代码,因此暂停期间不会执行函数 main 中的第二行代码
  • 2s 后,函数 slowFunc 打印一行文本再返回
  • 控制权返回到 main 函数,因此执行第二行代码 —— 向终端打印一条消息

这些代码是阻塞的,因为等待函数 slowFunc() 返回期间,没有执行其他代码

使用 Goroutine 处理并发操作

Go 语言提供了 Goroutine,可在调用函数 slowFunc 后立即执行 main 函数中的第二行代码。这种情况下函数 slowFunc 依然会执行,但不会阻塞程序中的其他代码行的执行

Goroutine 使用非常简单,只需要在 Goroutine 执行的函数或者方法前加上关键字 go 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"time"
)

func slowFunc() {
time.Sleep(time.Second * 2)
fmt.Println("sleeper() finished")
}

func main() {
go slowFunc()
fmt.Println("I am now show straightaway !")
}

执行后,不能看到调用 slowFunc 的结果,这是因为 Goroutine 立即返回。这意味着程序将接着执行后面的代码,然后退出。如果没有其他因素阻止,则程序将在 Goroutine 返回前就退出。

下面代码将演示,如何利用 Goroutine 来实现并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func slowFunc() {
time.Sleep(time.Second * 2)
fmt.Println("sleeper() finished")
}

func main() {
go slowFunc()
fmt.Println("I am now show straightaway !")
time.Sleep(time.Second * 3)
}

执行后的输出则符合预期:

1
2
3
I 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!