教程 > Go 教程 > Go 高级 阅读:376

Go 语言 Goroutines 协程并发

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


什么是 Goroutines?

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


Goroutines 相对于线程的优势

  • 与线程相比,Goroutines 非常小。它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要增长和缩小,而在线程的情况下,堆栈大小必须指定并固定。
  • Goroutines 被多路复用到较少数量的 OS 线程。一个包含数千个 Goroutine 的程序中可能只有一个线程。如果该线程块中的任何 Goroutine 要等待用户输入,则创建另一个 OS 线程,并将剩余的 Goroutine 移动到新的 OS 线程。所有这些都由运行时处理,我们作为程序员从这些复杂的细节中抽象出来,并获得了一个干净的 API 来处理并发。
  • Goroutine 使用通道(channel)进行通信。通道的设计可以防止在使用 Goroutine 访问共享内存时发生竞争条件。通道可以被认为是一个管道,Goroutines 使用它进行通信。我们将在下一个章节中详细讨论通道。

如何启动一个 Goroutine?

使用关键字为函数或方法调用添加前缀go,这样就会有一个新的 Goroutine 同时运行。

让我们创建一个 Goroutine :)

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    go hello()
    fmt.Println("main function")
}

运行示例

go hello()启动一个新的Goroutine。现在 hello() 函数将与main()函数同时运行。main() 函数在它自己的 Goroutine 中运行,它被称为main Goroutine(主协程)。

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

该程序输出结果如下

main function

这个程序只输出文本 main function。我们启动的 Goroutine 发生了什么?我们需要了解 goroutine 的两个主要属性才能理解为什么会发生这种情况。

  • 当一个新的 Goroutine 启动时,goroutine 调用会立即返回。与函数不同的是,控件不会等待 Goroutine 完成执行。在 Goroutine 调用之后,控制立即返回到下一行代码,并且忽略来自 Goroutine 的任何返回值。
  • 主 Goroutine 应该正在运行,以便任何其他 Goroutine 运行。如果主协程终止,则程序将终止,并且不会运行其他 Goroutine。

我想现在能够理解为什么我们的 Goroutine 没有运行。调用 go hello() 后。控制器立即返回下一行代码,无需等待运行 hello() 函数的 goroutine执行完成。然后主协程打印 “main function”。接着主 Goroutine 终止,因为没有其他代码可以执行,因此运行 hello() 函数的 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")
}

运行示例

在上面程序中,我们调用了time包的 Sleep()方法,该方法使正在执行的 go 协程休眠。在这种情况下,主 goroutine 会休眠 1 秒。现在运行 hello() 的 goroutine 有足够的时间在主 Goroutine 终止之前执行。该程序首先打印,等待 1 秒然后主 goroutine 打印。

上述程序执行结果如下

Hello world goroutine
main function

这种在主 Goroutine 中使用 sleep 来等待其他 Goroutines 完成执行的方式是我们用来理解 Goroutines 如何工作的一种技巧。通道可用于阻塞主 Goroutine,直到所有其他 Goroutine 完成它们的执行。我们将在下一个章节中讨论Go 语言中的通道(channel)


启动多个 Goroutine

让我们再编写一个启动多个 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")
}

运行示例

上面的程序中启动了两个 Goroutine。这两个 Goroutine 现在并发运行。numbers协程最初休眠250毫秒,然后打印 1,然后再次休眠并打印2。依次进行类似的执行,直到它打印5。类似地alphabets协程打印的字母从a到e并且具有400毫秒的睡眠时间。主协程启动numbersalphabets两个协程,休眠 3000 毫秒然后打印“main terminated”终止。

上述代码执行结果如下

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

下图描述了该程序的工作原理。

Goroutines 程序工作原理
Goroutines 程序工作原理

图中第一个蓝色部分代表的是numbers()协程。第二个栗色部分代表alphabets() 协程。第三个绿色部分代表主协程。最后的黑色部分是上面几个协程合并的运行过程。

每个框顶部的 0 ms、250 ms 等字符串表示以毫秒为单位的时间,每个框底部的输出表示为 1、2、3 等。 蓝色框告诉我们 1 在 250 ms 后打印,2 在 500 ms 后打印,依此类推。

最后一个黑框底部的值是 1 a 2 3 b 4 c 5 d e main terminate,这也是程序的输出。

Go 多协程并发运行
Go 多协程并发运行

我们通过上面动图来实际观察其打印的过程。

查看笔记

扫码一下
查看教程更方便