1 概述
Go 语言中的协程是由 Go 运行时(runtime)调度器(scheduler)进行管理和调度的。
当程序启动时,Go 运行时会默认启动一个主协程。主协程会创建其他的子协程,这些协程会被分配到不同的系统线程上进行执行。当某个协程发生阻塞时,Go 运行时会将该协程挂起并让出 CPU,转而执行其他协程,以充分利用系统资源。
2 go关键字
在 Go 语言中,创建协程非常简单,只需要在函数调用前加上 go 关键字即可。我们来试试看:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("before go A at: " + time.Now().String())
go A()
fmt.Println("after go A at: " + time.Now().String())
return
}
func A() {
fmt.Println("inside go A at" + time.Now().String())
}
输出
before go A at: 2024-03-22 20:06:54.811707 +0800 CST m=+0.000095334
after go A at: 2024-03-22 20:06:54.811881 +0800 CST m=+0.000270001
很奇怪,并没有输出 新起的那个协程的打印。
原因有两点:
- 发起协程并不是阻塞的,发起之后,当前协程继续往下走,并不会等待新起协程所调用的方法执行完;
A() // 调用A函数,等待A函数执行完的返回结果
go A() // 发起协程执行A函数,不等待A函数执行,当前协程直接往下走
- main方法所在的协程一直在执行,直到最后return也没有让出过,导致新发起的协程并没有机会被执行。
2.1 协程的执行
而我们知道有很多方法可以触发让出,比如系统调用。
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("before go A at: " + time.Now().String())
go A()
time.Sleep(1 * time.Second)
fmt.Println("after go A at: " + time.Now().String())
return
}
func A() {
fmt.Println("inside go A at: " + time.Now().String())
}
输出
before go A at: 2024-03-22 20:08:13.069353 +0800 CST m=+0.000218709
inside go A at: 2024-03-22 20:08:13.069625 +0800 CST m=+0.000491626
after go A at: 2024-03-22 20:08:14.071853 +0800 CST m=+1.002744001
我们在 go A() 后面加了一个 time.Sleep(),触发了系统调用,主函数的协程让出,从而执行了协程。
2.2 用匿名函数发起协程
当然,我们这里 go关键字后面调用的是一个 命名函数A,其实也可以直接调用 匿名函数。
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("before go func at: " + time.Now().String())
go func() {
fmt.Println("inside go func at: " + time.Now().String())
}()
time.Sleep(1 * time.Second)
fmt.Println("after go func at: " + time.Now().String())
return
}
输出
before go func at: 2024-03-22 20:13:05.183352 +0800 CST m=+0.000230251
inside go func at: 2024-03-22 20:13:05.183605 +0800 CST m=+0.000482959
after go func at: 2024-03-22 20:13:06.188708 +0800 CST m=+1.005603459
效果是一样的。
2.3 多个协程的执行
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("before go func at: " + time.Now().String())
go func() {
fmt.Println("inside go func1 at: " + time.Now().String())
}()
go func() {
fmt.Println("inside go func2 at: " + time.Now().String())
}()
go func() {
fmt.Println("inside go func3 at: " + time.Now().String())
}()
time.Sleep(1 * time.Second)
fmt.Println("after go func at: " + time.Now().String())
return
}
输出
before go func at: 2024-03-22 20:32:20.556914 +0800 CST m=+0.000219418
inside go func3 at: 2024-03-22 20:32:20.557247 +0800 CST m=+0.000552293
inside go func1 at: 2024-03-22 20:32:20.557248 +0800 CST m=+0.000554251
inside go func2 at: 2024-03-22 20:32:20.557253 +0800 CST m=+0.000558793
after go func at: 2024-03-22 20:32:21.560869 +0800 CST m=+1.004190918
这里我们看到,连续发起了3个协程,当前协程让出后,这3个协程的执行顺序是不固定的。这是因为go的调度器只是尽可能地让每个协程都有均等的机会被调度到并被执行,而并不能保证它们的执行顺序。
2.4 多次让出
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("before go func at: " + time.Now().String())
go func() {
fmt.Println("inside go func1 before sleep at: " + time.Now().String())
time.Sleep(10 * time.Millisecond)
fmt.Println("inside go func1 after sleep at: " + time.Now().String())
}()
go func() {
fmt.Println("inside go func2 before sleep at: " + time.Now().String())
time.Sleep(20 * time.Millisecond)
fmt.Println("inside go func2 after sleep at: " + time.Now().String())
}()
go func() {
fmt.Println("inside go func3 before sleep at: " + time.Now().String())
time.Sleep(30 * time.Millisecond)
fmt.Println("inside go func3 after sleep at: " + time.Now().String())
}()
time.Sleep(20 * time.Millisecond)
fmt.Println("after go func at: " + time.Now().String())
return
}
输出
before go func at: 2024-03-22 20:41:51.424324 +0800 CST m=+0.000125417
inside go func1 before sleep at: 2024-03-22 20:41:51.424463 +0800 CST m=+0.000264251
inside go func3 before sleep at: 2024-03-22 20:41:51.424489 +0800 CST m=+0.000289917
inside go func2 before sleep at: 2024-03-22 20:41:51.424481 +0800 CST m=+0.000282792
inside go func1 after sleep at: 2024-03-22 20:41:51.436992 +0800 CST m=+0.012793751
inside go func2 after sleep at: 2024-03-22 20:41:51.446344 +0800 CST m=+0.022145626
after go func at: 2024-03-22 20:41:51.446349 +0800 CST m=+0.022150876
可以看到:
- 每次协程遇到让出的时候,都会暂停执行,让出给其他协程执行;
- 当其他协程执行完之后,也并不是当然地回到当前协程继续执行,有可能会调度到第三个协程;
- func3的after语句没有被打印出来,原因是当func2 after执行完后,CPU调度到了main函数所在的协程;而main函数协程执行完了之后,程序就退出了,其他的协程都被直接打断了;
至此,我们简单地了解了go的协程的一些运行情况。