Go 协程是什么
goroutine是Go语言并行设计的核心。goroutine说到底就是线程,但是它比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,Go 语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
Go 协程(goroutine)相比于线程(thread)的优势
- 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
- Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
- Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道。
如何启动一个 Go 协程
goroutine是通过Go语言runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数在前面加上关键字go。
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
在第 11 行,go hello()
启动了一个新的 Go 协程。现在 hello()
函数与 main()
函数会并发地执行。主函数会运行在一个特有的 Go 协程上,它称为 Go 主协程(Main Goroutine)。
运行一下程序,你会很惊讶!
该程序只会输出文本 main function
。我们启动的 Go 协程究竟出现了什么问题?要理解这一切,我们需要理解两个 Go 协程的主要性质。
- 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
- 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。
现在你应该能够理解,为何我们的 Go 协程没有运行了吧。在第 11 行调用了 go hello()
之后,程序控制没有等待 hello
协程结束,立即返回到了代码下一行,打印 main function
。接着由于没有其他可执行的代码,Go 主协程终止,于是 hello
协程就没有机会运行了。
我们现在修复这个问题。
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 协程。在这里,我们使 Go 主协程休眠了 1 秒。因此在主协程终止之前,调用 go hello()
就有足够的时间来执行了。该程序首先打印 Hello world goroutine
,等待 1 秒钟之后,接着打印 main function
。
在 Go 主协程中使用休眠,以便等待其他协程执行完毕,这种方法只是用于理解 Go 协程如何工作的技巧。信道可用于在其他协程结束执行之前,阻塞 Go 主协程。我们会在下一教程中讨论信道。
启动多个 Go 协程
为了更好地理解 Go 协程,我们再编写一个程序,启动多个 Go 协程。
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 行,启动了两个 Go 协程。现在,这两个协程并发地运行。numbers
协程首先休眠 250
微秒,接着打印 1
,然后再次休眠,打印 2
,依此类推,一直到打印 5
结束。alphabete
协程同样打印从 a
到 e
的字母,并且每次有 400
微秒的休眠时间。 Go 主协程启动了 numbers
和 alphabete
两个 Go 协程,休眠了 3000
微秒后终止程序。
1 a 2 3 b 4 c 5 d e main terminated