Goroutines
Q7nl1s admin

在上一篇教程中,我们讨论了并发性以及它与并行性的区别。在本教程中,我们将讨论如何在Go中使用Goroutines实现并发。

什么是Goroutines?

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

相对于线程,Goroutines的优势

  • 与线程相比,Goroutines非常便宜。它们的堆栈大小只有几kb,而且堆栈可以根据应用程序的需要进行增减,而在线程的情况下,堆栈大小必须被指定并且是固定的。
  • Goroutines被复用到数量较少的操作系统线程中。在一个有数千个Goroutine的程序中,可能只有一个线程。如果该线程中的任何一个Goroutine阻塞了,例如等待用户输入,那么就会创建另一个OS线程,其余的Goroutine被转移到新的OS线程。所有这些都是由运行时处理的,而我们作为程序员被从这些复杂的细节中抽象出来,并被赋予一个干净的API来处理并发问题。
  • 线程使用通道进行通信。通道的设计可以防止在使用Goroutines访问共享内存时发生竞赛条件。通道可以被认为是一个channels,Goroutines使用它进行通信。我们将在下一个教程中详细讨论channels。

如何启动一个Goroutine?

在函数或方法的调用前加上关键字go,你就会有一个新的Goroutine同时运行。

让我们来创建一个Goroutine :)

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

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

在第11行,go hello()启动了一个Goroutine。现在hello()函数将与main()函数一起并发运行。主函数在它自己的Goroutine中运行,它被称为主Goroutine。

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

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

  • 当一个新的Goroutine被启动时,Goroutine的调用立即返回。与函数不同,控件不会等待Goroutine执行完毕。控件会立即返回到Goroutine调用后的下一行代码,Goroutine的任何返回值都会被忽略。
  • 主Goroutine应该在运行中,其他Goroutine才能运行。如果主Goroutine终止了,那么程序就会被终止,其他Goroutine就不会运行。

我想现在你应该能够理解为什么我们的Goroutine没有运行了。在调用了第11行的hello()之后,控制权立即返回到主函数的下一行代码,没有等待hello goroutine完成并打印main function。然后main Goroutine终止了,因为没有其他代码可以执行,因此hello Goroutine没有机会运行。

现在我们来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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")
}

在上述程序的第13行,我们调用了时间包的Sleep方法,使正在执行的go程序处于睡眠状态。在这种情况下,main goroutine被睡眠1秒钟。现在对go hello()的调用在主Goroutine终止之前有足够的时间来执行。这个程序首先打印出Hello world goroutine,等待1秒钟,然后打印出main函数。

这种在主Goroutine中使用sleep来等待其他Goroutine完成执行的方式是我们用来理解Goroutine如何工作的一个黑客。通道可以用来阻止主Goroutine,直到所有其他Goroutine完成它们的执行。我们将在下一个教程中讨论通道。

启动多个Goroutine

让我们再写一个启动多个Goroutine的程序来更好地理解Goroutine。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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")
}

上述程序在第21行和第22行启动了两个Goroutine。这两个Goroutine现在同时运行。numbers Goroutine最初睡眠250毫秒,然后打印1,然后再次睡眠并打印2,同样的循环发生,直到它打印5。同样地,alphabets Goroutine打印从ae的字母,每次执行有400毫秒的睡眠时间。main Goroutine启动了numbersalphabets Goroutine,睡眠时间为3000毫秒,然后终止。

这个程序的输出:

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

下面的图片描述了这个程序如何工作。请在新的标签页中打开图片,以获得更好的视觉效果 :)

Goroutines-explained

图片的第一部分用蓝色表示numbers Goroutine,第二部分用栗色表示alphabets Goroutine,第三部分用绿色表示main Goroutine,最后一部分用黑色表示合并上述三部分,向我们展示了程序的工作原理。每个方框顶部的0毫秒、250毫秒等字符串代表了以毫秒为单位的时间,每个方框底部的输出表示为1、2、3,等等。蓝色方框告诉我们,250 ms后打印1500 ms后打印2,以此类推。最后一个黑框的底部有数值1 a 2 3 b 4 c 5 d e main terminated,这也是程序的输出。这张图是不言自明的,你将能够理解这个程序的工作原理。

这就是Goroutines的内容了。祝你有一个愉快的一天。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View