什么是 Goroutines?
Goroutines 是与其他函数(方法)同时运行的函数(方法)。 Goroutines 可以被认为是轻量级线程。与线程相比,创建 Goroutine 的成本很小。因此,Go 应用程序通常会同时运行数千个 Goroutines。
Goroutines 对比线程的优势
与线程相比,goroutine 开销非常小。它们的堆栈大小只有几个 kb,并且堆栈可以根据应用程序的需要扩大或者缩小,而在线程的情况下,堆栈大小必须指定并且是固定的。
Goroutines 被多路复用到较少数量的 OS 线程。程序中可能只有一个线程,但是可以有上千个 Goroutines。如果线程块中的任何 Goroutine 等待用户输入,则创建另一个 OS 线程并将剩余的 Goroutines 移到新的 OS 线程。所有这些都是在运行时处理,我们作为程序员从这些复杂的细节中抽象出来,并给出一个干净的 API 来处理并发。
Goroutines 使用 channels 进行通信。channels 可防止使用 Goroutines 访问共享内存时发生竞争条件。channels 可以被认为是 Goroutines 通信的管道。我们将在下一个教程中详细讨论 channels。
如何开启一个Goroutine?
在函数或方法调用前面加上关键字 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 function
。我们启动的 Goroutine 发生什么了?我们需要了解 Goroutine 的两个主要属性来了解为什么会发生这种情况。
当一个新的 Goroutine 启动时,Goroutine 调用立即返回。与函数不同,主程序不等待 Goroutine 完成执行。主程序在 Goroutine 调用之后立即返回到下一行代码,并且忽略 Goroutine 中的任何返回值。
任何的 Goroutines 都跑在**主程序的 Goroutine 上**运行。如果主程序 Goroutine 终止,那么程序将被终止,没有其他 Goroutine 将运行。
我想现在你将能够理解为什么我们的 Goroutine 没有跑起来。第 11 行调用完 go hello()
后。 主程序立即返回到下一行代码,不会等待 goroutine hello 完成,所以输出 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")
}
在上面的程序第 14 行,我们调用了 time 包的 Sleep 方法,它能程序休眠。在这个程序中,将主的 goroutine 休眠 1 秒。现在,go hello()
有足够的时间在主 Goroutine 终止之前执行。这个程序首先打印 Hello world goroutin
,等待 1 秒,然后打印 main function
。
这种在主程序 Goroutine 中使用 sleep 来等待其他 Goroutine 完成执行的方法是一种 hack 手段,我们使用它主要是来了解 Goroutine 是如何工作的。channels 也可以用来阻塞主 Goroutine,直到所有其他的 Goroutine 执行完毕。我们将在下一篇教程中讨论 channels 。
_
启动多个Goroutines
让我们再写一个程序,启动多个 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")
}
上面的程序 23、24 行中启动了两个 Goroutines。这两个 Goroutines 现在同时运行。Goroutine numbers
最初睡眠 250 毫秒然后输出 1
,然后再次睡眠并输出 2
。并且相同的循环发生直到它输出 5。类似地,Goroutine alphabets
从 a 到 e 输出字母并且每次是 400 毫秒的时间间隔。主程序 Goroutine 启动 numbers
和 alphabets
Goroutines,睡眠 3000 毫秒,然后终止程序。
程序输出
1 a 2 3 b 4 c 5 d e main terminated
下图描绘了该程序的工作原理。请在新标签页中打开图片以获得更好的图片体验:)
蓝色图像的第一部分代表 numbers Goroutine,栗色的第二部分代表 alphabets Goroutine,绿色的第三部分代表主程序 Goroutine,黑色的最后部分合并以上三个来告诉我们程序是如何工作的。每个框顶部的 0 ms,250 ms 等字符串表示以毫秒为单位的时间,输出在每个框的底部表示为 1,2,3 等等。蓝色框告诉我们在 250 ms
后打印 1
,在 500 ms
后打印2
,依此类推。最后一个黑框的底部的值为 1 a 2 3 b 4 c 5 d e main terminated
,这也是该程序的输出。图像是我自己画的,希望你能够理解该程序的工作原理。