goroutine
到现在为止,程序都是一件一件往下做:上一件做完,才做下一件。下载三个文件,就是一个接一个地下:
1package main2 3import "fmt"4 5func download(file string) {6 fmt.Println("下好了", file)7}8 9func main() {10 download("photo.jpg")11 download("music.mp3")12 download("video.mp4")13 fmt.Println("全部下完了")14}
下好了 photo.jpg 下好了 music.mp3 下好了 video.mp4 全部下完了
真的下载要等网络,一个文件等几秒,三个就是三倍。这三件事互不相干,完全可以同时做。Go 让几件事同时做的办法是 goroutine:可以把它想成程序里另开的一条「做事的线」,main 是一条,你还可以再开很多条。
go:另开一个 goroutine
在一次函数调用前面写上 go,这次调用就放到一个新的 goroutine 里去跑。main 不等它,马上接着往下走:
1package main2 3import "fmt"4 5func download(file string) {6 fmt.Println("下好了", file)7}8 9func main() {10 fmt.Println("开始下载")11 go download("photo.jpg")12}
开始下载
只显示了「开始下载」,download 里的那一行没有出现。点单步看看:走过 go 那一行,右边多出一条泳道,这是新的 goroutine,它标着「等着轮到它」;main 那条标着「在跑」。接着 main 走到最后的 },程序结束了,新的 goroutine 一步都没走。
main 结束,程序就结束
main 自己也跑在一个 goroutine 里:前几课 panic 时见过的 goroutine 1 [running]: 就是它。main 一返回,整个程序就结束,别的 goroutine 不管做到哪儿,都跟着结束。Go 不会等它们。
注意:go 只是说「这件事可以开始了」,不保证它什么时候跑。在真的电脑上,几个 CPU 同时干活,新的 goroutine 偶尔也能抢在 main 结束前跑上几步。所以既不能指望它「跑不了」,也不能指望它「跑得完」:要等它,就得明明白白地等。
先睡一会儿?
最直接的想法:让 main 晚一点结束。time.Sleep(时长) 让当前的 goroutine 停一段时间;time.Second 是 1 秒,100 * time.Millisecond 是 0.1 秒:
1package main2 3import (4 "fmt"5 "time"6)7 8func download(file string) {9 fmt.Println("下好了", file)10}11 12func main() {13 fmt.Println("开始下载")14 go download("photo.jpg")15 time.Sleep(time.Second)16 fmt.Println("main 结束")17}
开始下载 下好了 photo.jpg main 结束
这次三行都有了。点单步:走到 time.Sleep,main 那条泳道变成「在睡」,新的 goroutine 这才轮到,跑完 download;然后 main 醒过来,显示最后一行。
注意:网页上的 time.Sleep 不真的等:所有 goroutine 都停下来的时候,时钟直接拨到下一个该醒的时刻。在你自己的电脑上,它会真的等 1 秒。
问题是:等多久才够?下载要多久是说不准的。下面每个文件要下 0.3 秒,main 等 0.1 秒:
这段程序显示什么?
1package main2 3import (4 "fmt"5 "time"6)7 8func download(file string) {9 time.Sleep(300 * time.Millisecond)10 fmt.Println("下好了", file)11}12 13func main() {14 for _, file := range []string{"a.txt", "b.txt", "c.txt"} {15 go download(file)16 }17 time.Sleep(100 * time.Millisecond)18 fmt.Println("不等了")19}
三个 goroutine 都开始了,可它们要睡 0.3 秒;main 0.1 秒就醒了,显示「不等了」,然后结束。main 一结束,还在睡的三个 goroutine 一起被结束,一行「下好了」都没有。
等短了,结果出不来;等长了,程序又白白多等。用 Sleep 等,其实是在猜时间。下一课的 channel 能让 main 正好等到 goroutine 做完,不多也不少。
几个 goroutine 一起做
go 后面也可以是一个匿名函数(第 16 课讲过),写完函数体马上用 () 调用它。下面给每个数开一个 goroutine,算它的平方,写进 squares 里属于它的那一格:
1package main2 3import (4 "fmt"5 "time"6)7 8func main() {9 nums := []int{3, 5, 8, 10}10 squares := make([]int, len(nums))11 for i, n := range nums {12 go func() {13 squares[i] = n * n14 }()15 }16 time.Sleep(100 * time.Millisecond)17 fmt.Println(squares)18}
[9 25 64 100]
点单步,看循环走完以后右边有五条泳道:main 和四个排队的 goroutine。main 一睡,它们一个接一个地跑,squares 的格子一个个填上。每一轮的 i 和 n 都是新的变量(Go 1.22 起),所以每个 goroutine 拿到的是自己那一轮的值。
不过 Sleep 并不是真正的同步:main 只是睡了一会儿,并没有确认它们写完。在自己电脑上用 go run -race(第 22 课)运行,这里会被报成数据竞争。从下一课起,就不再用 Sleep 等了。
为什么不让每个 goroutine 自己显示结果?因为先后说不准:
1for _, n := range nums {2 go func() {3 fmt.Println(n * n)4 }()5}
在真的电脑上,几个 goroutine 真的同时跑,谁先显示每次可能不一样:这次 9 25 64 100,下次也许 64 9 100 25。网页上的引擎一次只让一个 goroutine 跑(跑到它停下来为止),所以每次都一样,别被它骗了。这门课的例子都让结果和先后无关:每个 goroutine 写自己的那一格,最后由 main 一起显示。
go 后面是一次调用
go 开出去的函数,返回值没人接,直接扔掉。所以不能这样拿结果:
1package main2 3import "fmt"4 5func square(n int) int {6 return n * n7}8 9func main() {10 result := go square(7)11 fmt.Println(result)12}
# command-line-arguments
./prog.go:10:12: syntax error: unexpected keyword go, expected expression发生了什么: go 语句没有值,不能放在 = 右边或者当参数:go 出去的函数的结果,要用 channel 拿回来。
go square(7) 是一条语句,不是一个值,放在 := 右边就是语法错误。另一个常见的错是匿名函数后面忘了 ():
1package main2 3import "fmt"4 5func main() {6 go func() {7 fmt.Println("下好了")8 }9}
# command-line-arguments
./prog.go:6:5: expression in go must be function call发生了什么: go 后面要写一次函数调用(带括号):go f(),或者 go func() { … }()。
expression in go must be function call:go 后面要的是一次调用,这里只写了一个函数,没有调用它。在 } 后面加上 () 就好了。想从 goroutine 拿回结果,可以像上一节那样写进一个变量,更好的办法是下一课的 channel。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。这一课还没有正式的等法,练习里先用 time.Sleep。它不是真正的同步,go run -race 会把这些程序报成数据竞争;从下一课起就不用 Sleep 等了。
修好它:合计还是 0
✓showTotals 给每张订单开一个 goroutine 算合计,可显示出来都是 0。改好它,让它显示每张订单真正的合计。程序算了两批订单。
合计 [50 100] 合计 [10 40 20]
1package main2 3import "fmt"4 5func sum(prices []int) int {6 total := 07 for _, p := range prices {8 total += p9 }10 return total11}12 13func showTotals(orders [][]int) {14 totals := make([]int, len(orders))15 for i, prices := range orders {16 go func() {17 totals[i] = sum(prices)18 }()19 }20 fmt.Println("合计", totals)21}22 23func main() {24 showTotals([][]int{{12, 30, 8}, {99, 1}})25 showTotals([][]int{{5, 5}, {40}, {7, 3, 10}})26}
一群工人算平方和
✓写好 sumOfSquares:给每个数开一个 goroutine,把它的平方写进 squares 里对应的那一格;等一会儿以后,把 squares 加起来返回。程序会显示两组数的平方和。
14 41
1package main2 3import (4 "fmt"5 "time"6)7 8func sumOfSquares(nums []int) int {9 squares := make([]int, len(nums))10 // 在这里开 goroutine11 time.Sleep(100 * time.Millisecond)12 total := 013 // 在这里把 squares 加起来14 return total15}16 17func main() {18 fmt.Println(sumOfSquares([]int{1, 2, 3}))19 fmt.Println(sumOfSquares([]int{4, 5}))20}
修好它:go 拿不到返回值
✓countBoth 想同时数两句话各有几个词,可它编译不过。改成:两个 goroutine 各数一句,把结果写进 a 和 b,等一会儿再返回。程序数了两对句子。
4 6 1 4
1package main2 3import (4 "fmt"5 "strings"6)7 8func countWords(s string) int {9 return len(strings.Fields(s))10}11 12func countBoth(s1, s2 string) (int, int) {13 a := go countWords(s1)14 b := go countWords(s2)15 return a, b16}17 18func main() {19 fmt.Println(countBoth("the order has shipped", "your download will start in seconds"))20 fmt.Println(countBoth("done", "please try again later"))21}
这一课学会了
go f(x)把这次调用放进一个新的 goroutine,main不等它,接着往下走。main也是一个 goroutine;main结束,整个程序结束,别的 goroutine 跟着结束。- 单步时每个 goroutine 一条泳道:「在跑」「等着轮到它」「在睡」。
go func() { … }():匿名函数后面要有();go语句没有值,拿不到返回值。time.Sleep等是在猜时间,等短了结果没出来,等长了白等;下一课用 channel 正好等到做完。- 几个 goroutine 的先后说不准:让每个 goroutine 写自己的那一格,等它们都做完(下一课)再一起显示。