[译] part 21: golang goroutines

1,057 阅读4分钟

在前面的教程中,我们讨论了并发以及它与并行的不同之处。在本教程中,我们将讨论如何使用Goroutines在 Go 中实现并发性。

什么是Goroutines

Goroutines是与其他函数或方法同时运行的函数或方法。 Goroutines可以被认为是轻量级线程。与线程相比,创建Goroutine的成本很小。因此,Go 应用程序通常可以轻松运行数千个Goroutines

Goroutines相较线程的优势

  • 与线程相比,Goroutines非常轻量化。它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要而伸缩,而在线程的情况下,堆栈必须固定指定大小。
  • Goroutines被复用到较少数量的系统线程。程序中可能一个线程有数千个Goroutines。如果该线程中的任何Goroutine阻塞,则创建另一个系统线程,并将剩余的Goroutines移动到新的线程。所有这些都由运行时处理,Go 从这些复杂的细节中抽象出来一个简洁的 API 来原生支持并发。
  • Goroutines使用channel进行通信。Goroutines使用channel通信可以避免因访问共享内存而发生竞态条件。channel可以被认为是Goroutines通信的管道。我们将在下一个教程中详细讨论channel

如何启动Goroutines

使用关键字go对函数或方法调用进行前缀修饰,就可以运行新的Goroutine了。

来创建一个Goroutine 吧:)

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    fmt.Println("main function")
}

Run in playgroud

在第 11 行,go hello()启动了一个新的Goroutine。现在hello函数将与main函数一起并发运行。main在其自己的Goroutine中运行,并把main函数执行的Goroutinemain Goroutine主协程。

运行这个程序,你会有一个惊喜!

该程序仅输出了main函数的文本。我们启动的Goroutine怎么了?我们需要了解go协程的两个主要属性,就知道为什么会发生这种情况了。

  • 当一个新的Goroutine启动时,Goroutine调用立即返回。与函数不同,控制器不会等待Goroutine完成执行。在Goroutine调用之后,控制器立即返回到下一行代码,并忽略了Goroutine的任何返回值。
  • main Goroutine控制该进程的任何其他Goroutines运行。如果main Goroutine终止,那么程序将被终止,其他Goroutine将可能得不到运行。

我猜你能够理解为什么我们的Goroutine没有被执行。在第 11 行调用go hello(),控制器立即执行下一行代码并不等待hello goroutine执行,然后打印了main function之后main Goroutine终止。没有等待时间,因此你的Goroutine没有时间执行。

我们简单解决一下这个问题。

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

Run in playgroud

在上面程序的第 13 行,我们调用了time包的Sleep方法,该方法让正在执行它的go协程休眠。在这种情况下,main Goroutine进入休眠状态 1 秒钟。现在调用go hello()有足够的时间在main Goroutine终止之前执行。该程序首先打印Hello world goroutine,然后等待 1 秒然后打印main function

这种在mian Goroutine中使用sleep等待其他Goroutines完成执行的方式只是我们用来理解Goroutines如何工作的,正常情况下肯定不能这么做。channels可用于阻塞main Goroutine,直到所有其他Goroutines完成执行。我们将在下一个教程中讨论channel

启动多个Goroutines

再写一个程序,启动多个Goroutines以更好地理解它。

package main

import (
    "fmt"
    "time"
)

func numbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

Run in playgroud

上面的程序在 21 和 22 行分别启动了两个Goroutines,这两个Goroutines同时运行。numbers Goroutine最初睡眠 250 毫秒然后打印 1,然后再次睡眠并打印 2,并且循环直到它打印 5。类似地,alphabets Goroutine从 a 到 e 打印字母并且是 400 毫秒的睡眠时间。main Goroutine在启动alphabetsnumbers Goroutines后睡眠 3000 毫秒,然后终止。

输出,

1 a 2 3 b 4 c 5 d e main terminated

下图描绘了该程序的工作原理。请在新标签页中打开图片以获得更好的可视性 :)

蓝色图像的第一部分代表numbers Goroutine,栗色的第二部分代表alphabets Goroutine,绿色的第三部分代表main Goroutine,黑色的合并了上述三个并向我们展示如何程序如何执行的。每个框顶部的 0 ms,250 ms 等字符串表示以毫秒为单位的时间,输出在每个框的底部,例如 1, 2, 3 等等。蓝色框告诉我们在 250 毫秒时打印 1,在 500 毫秒时打印 2,依此类推。黑色框底部的值为 1 a 2 3 b 4 c 5 d e 然后main终止,这也是程序的输出。希望能通过这个图理解该程序的工作原理。