- 原文地址:Part 21: Goroutines
- 原文作者:Naveen R
- 译者:咔叽咔叽 转载请注明出处。
在前面的教程中,我们讨论了并发以及它与并行的不同之处。在本教程中,我们将讨论如何使用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")
}
在第 11 行,go hello()
启动了一个新的Goroutine
。现在hello
函数将与main
函数一起并发运行。main
在其自己的Goroutine
中运行,并把main
函数执行的Goroutine
为main 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")
}
在上面程序的第 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")
}
上面的程序在 21 和 22 行分别启动了两个Goroutines
,这两个Goroutines
同时运行。numbers Goroutine
最初睡眠 250 毫秒然后打印 1,然后再次睡眠并打印 2,并且循环直到它打印 5。类似地,alphabets Goroutine
从 a 到 e 打印字母并且是 400 毫秒的睡眠时间。main Goroutine
在启动alphabets
和numbers 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
终止,这也是程序的输出。希望能通过这个图理解该程序的工作原理。