1.0 问题引入
\qquad
作为golang的学习者,尤其是对于没有什么开发经验的学生,可能对于golang中一些并发的库比较难理解,需要一些具体的场景帮助理解:
\qquad
首先看下面一段代码:
func hello() {
fmt.Println("hello,world")
go hello_loop()
time.Sleep(time.Second * 5)
fmt.Println("hello exit")
}
func hello_loop() {
for range time.Tick(time.Second) {
fmt.Println("running")
}
}
func main() {
go hello()
time.Sleep(20 * time.Second)
}
\qquad
main函数退出时会结束所有正在执行的函数,这是基本的常识,但在服务运行时,main函数实际永远在运行中,所以我们用time.sleep()
来模拟服务的运行过程,当用户需要业务时,服务通过创建新的goroutine
来为用户服务,这里使用hello()
来模拟
\qquad
这段代码输出的结果显而易见
hello,world
running
running
running
running
running
hello exit
running
running
running
…
\qquad
可以看出虽然在函数调用(协程启动)是具有父子关系,但是子协程并不会随着父线程的退出而退出,这一点在实际开发中极易造成协程泄漏与死锁
,也就是协程在后台无序运行或者被永久挂起,最终造成服务崩溃,因此需要对协程进行管理,而golang中的context库可以完成这一任务。
1.1 基本原理
\qquad
程序中协程的开启本身是一种树结构,但自然情况下协程一旦启动不会受到父协程的管理,完全处于一种无序状态
\qquad
context
的本意是上下文,放在这里就是想让协程开启时能够携带上下文信息,从而知道自己“父亲”的运行状态,从而接受他的管理,因此我们想在开启协程的同时,同步的构建一种表示协程关系树结构,来统一的管理各个协程。
\qquad
下面我们来对上面的代码进行改造,让大家直接感受到context库的作用:
func hello() {
//这一段是加入的代码
ctx, cancle := context.WithCancel(context.Background())
defer cancle() // 在函数退出时调用cancle()
//
fmt.Println("hello,world")
go hello_loop(ctx)
time.Sleep(time.Second * 5)
fmt.Println("hello exit")
}
func hello_loop(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-time.After(time.Second):
fmt.Println("running")
}
}
}
func main() {
go hello()
time.Sleep(20 * time.Second)
}
\qquad
这里的结果就不再赘述,进行改造后,子协程可以随着父协程的结束而退出,从而实现了协程的管理
\qquad
基本原理是我们将一个父协程创建的context.Context
类型的变量在创建子协程时传入子协程中,而这个变量中记录了父协程的运行状态(是否调用了cancel()
方法)
\qquad
这里注意代码中只实现了一层的结构,实际上context.WithCancel()
方法中的输出与输出都是context.Context
类型,因此可以嵌套使用,从而组成协程树