GoLang通过开启多个Goruntine来实现并发,典型的是在网络编程中,无论是RPC调用还是web服务,当Sever端收到一个网络请求request后,都将开启一个对应的Goruntine进行处理,由于服务需求的不同,有可能需要多个不同的Goroutine,出于并发效率和异步的考虑,这些Goroutine又有可能开启多个子routine进行工作,因此往往一个Request请求都将在Server端开启一个routine树,这时,对于routine树的管理工作变得至关重要:
上述工作需要设计并发控制来完成,在GoLang中,能够实现并发控制的主要方法有:
package main
import (
"fmt"
"time"
)
func main(){
ch1:=make(chan bool)
ch2:=make(chan bool)
go Worker1(ch1)
go Worker2(ch2)
time.Sleep(3*1e9)
ch1<-true
time.Sleep(3*1e9)
ch2<-true
}
func Worker1(ch chan bool){
for ;;{
select{
case <-ch:{
fmt.Println("Worker1: I was cancled by parent goruntinue")
return
}
default:
time.Sleep(1e9)
fmt.Println("Worker1: I am doing my work")
}
}
}
func Worker2(ch chan bool){
for ;;{
select{
case <-ch:{
fmt.Println("Worker2: I was cancled by parent goruntinue")
return
}
default:
time.Sleep(1e9)
fmt.Println("Worker2: I am doing my work")
}
}
}
可以看到,父routine(main)为子routine(worker1\worker2)分配了专用的通道(ch1\ch2)专用于并发控制,在子routine中,使用select语句判断父线程是否通过通道发送了关闭通知,否则将继续进行自己的工作,而父routine通过向chan发送一个值关闭子routine,这种方式虽然有效实现了控制操作,但存在很多问题:
而通过chan进行消息共享往往也是行不通的:
package main
import (
"fmt"
"time"
)
func main(){
ch:=make(chan int)
go func()(){
for ;;{
fmt.Println("Goroutine1:",<-ch)
}
}()
go func()(){
for ;;{
fmt.Println("Goroutine2:",<-ch)
}
}()
ch<-1
ch<-1
time.Sleep(5*1e9)
}
在以上的代码中,main同时开启两个routine工作,通同一个通道ch尝试向两个通道发送消息,运行可以看到:
虽然可以用更复杂的逻辑解决这些问题,但大大增加了编码的工作量,单单使用通道无法胜任复杂的并发控制工作。
package main
import (
"fmt"
"sync"
"time"
)
type request struct{
message string
m sync.Mutex
}
func main(){
req:=new(request)
req.message="helloworld"
go func(req *request)(){
req.m.Lock()
fmt.Println(req.message)
req.m.Unlock()
}(req)
go func(req *request)(){
req.m.Lock()
req.message="It's changed!"
req.m.Unlock()
}(req)
time.Sleep(3*1e9)
}
上述代码通过sync包提供的Mutex互斥锁操作,实现了多routine之间安全的共享同一消息,但当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低,不适合直接用于在复杂的routine树中实现消息共享。
于此同时,sync包提供了WaitGroup机制用于并发控制,但只适用于多个子协程平级并发处理一个任务的情况:
package main
import (
"fmt"
"sync"
"time"
)
func main(){
var wg sync.WaitGroup
wg.Add(2)
go func()(){
time.Sleep(2*1e9)
fmt.Println("Routine1 is done")
wg.Done()
}()
go func()(){
time.Sleep(2*1e9)
fmt.Println("Routine2 is done")
wg.Done()
}()
wg.Wait()
fmt.Println("It's all over")
}
综上两种方法可以看到,虽然通道机制和sync包在一定程度上都完成了多routine间消息共享和并发控制的目的,但都是很底层的实现,无法满足对于复杂routine树进行管理的需求。
context包正是基于通道机制和sync包进行设计,简化了在多个Goroutine传递上下文数据、中止routine树的等操作,使开发者能够更容易地完成并发控制。通过context的译名”上下文“,可以更好地理解这个概念,上下文,即"当前程序的执行状态",Context包通过构建树型关系的Context,来达到上一层Goroutine能对传递给下一层Goroutine的控制,一个Goroutine从其父进程中获得父进程给与的上下文状态信息,对这个上下文添加上自己的状态,交付给自己的子routine,最终形成了一棵context树,在上下文中包含的状态主要有:控制信息(运行剩余时间、截至日期、结束状态)、共享消息。
本部分分析了Context包设计的目标需求,列举了使用通道机制和sync包完成并发控制的方式和不足,总结了Context包的设计使命,下一部分将对Context包的结构和使用方法进行分析。
关于Context包的介绍,参照:
Go1.7(当前是RC2版本)已将原来的
golang.org/x/net/context
包挪入了标准库中,放在$GOROOT/src/context下面。标准库中net
、net/http
、os/exec
都用到了context
。同时为了考虑兼容,在原golang.org/x/net/context
包下存在两个文件,go17.go
是调用标准库的context
包,而pre_go17.go
则是之前的默认实现,其介绍请参考go程序包源码解读。
Context包是一个接口设计的范例,在对整个包功能进行高度提炼之后,最终只对外暴露了一个接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
具体使用方法为:
使用context.Backgroud()函数创建一个初始的context值,正如这个函数名一样,可以理解为以当前程序为上下文(背景)建立一个根context,该context没有deadline,没有value,没有err,不可被取消,被用来作为一个初始值,等待程序附加更多的上下文控制信息。
ctx:=context.Background()
fmt.Println(ctx.Deadline())//0001-01-01 00:00:00 +0000 UTC false
fmt.Println(ctx.Err())//nil
在实际使用中,不需要命名一个空context对象,而是将context.Backgroud()作为函数参数,如下面步骤所示。
由如下四个函数完成:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context
可以看到,上述四个函数均接收一个Context对象,返回一个Context对象,得益于Context包的高度封装,我们不需要关注其内部是否涉及到多种Context的派生类型(实际上是的),可以把这四个函数一视同仁的看作:作为包装函数,为一个父context节点附加更多的控制信息,得到该Context的子节点,在任意一层Goroutine程序中,均可使用这四个函数生成子节点,再向下传递,为routine树生成一个对应的context树,从而实现Goroutine树实现消息共享和控制。
这四个函数的具体使用实例如下:
1)WithCancel
该函数将父节点复制到子节点,返回一个额外的CancelFunc
函数类型变量,这一操作使Context变为可主动取消的,一旦调用生成的CancelFunc函数,该Context和其衍生的所有子孙Context都将被取消,context.Done()函数返回的通道将被闭,context.err()包含了context被取消的原因,实例如下:
package main
import (
"context"
"fmt"
"time"
)
func main(){
ctxWithCancel,cancelFunc:=context.WithCancel(context.Background())
go Worker1(ctxWithCancel,"hello")
go Worker2(ctxWithCancel,"hello")
time.Sleep(3*1e9)
cancelFunc() //调用函数取消ctxWithCancel
time.Sleep(3*1e9)
}
func Worker1(ctx context.Context,message string){
for ;;{
select{
case <-ctx.Done():{
fmt.Println("worker1 is cancled")
fmt.Println(ctx.Err()) //context canceled
return
}
default:
time.Sleep(2*1e9)
fmt.Println("worker1: ",message)
}
}
}
func Worker2(ctx context.Context,message string){
for ;;{
select{
case <-ctx.Done():{
fmt.Println("worker2 is cancled")
fmt.Println(ctx.Err()) //context canceled
return
}
default:
time.Sleep(2*1e9)
fmt.Println("worker2: ",message)
}
}
}
子Goroutine判断当前父传来的context是否是被取消的,做出对应的操作(最常见的退出routine,当触发某些条件时,可以调用CancelFunc对象来终止子结点树的所有routine。可以看到,对于被主动取消的context,Err()将简单的返回"context canceled"消息。
需要注意的是,context树的取消规则为:使用withCaneel函数生成的canelFunc,可以主动地取消其所有子孙context,在其后代中再次调用withCancel函数,这个性质不会被影响,将形成一个树形管理体系,如下图:
2)WIthDeadline
该函数同样父节点复制到子节点,返回一个额外的CancelFunc
函数类型变量,不同的是,该函数将指示context存活的最长时间。如果超过了过期时间,会自动撤销它的子context。所以context的生命期是由父context的routine和deadline共同决定的。如果父context同样具备一个deadline,那么实际的deadline将是两者的最小值,使用如下:
package main
import (
"context"
"fmt"
"time"
)
func main(){
ctxWithDeadline,_:=context.WithDeadline(context.Background(),time.Now().Add(2*1e9))
go Worker1(ctxWithDeadline,"hello")
go Worker2(ctxWithDeadline,"hello")
time.Sleep(3*1e9)
fmt.Println()
}
func Worker1(ctx context.Context,message string){
for ;;{
select{
case <-ctx.Done():{
fmt.Println("worker1 is cancled")
fmt.Println(ctx.Err()) //context canceled
return
}
default:
time.Sleep(2*1e9)
fmt.Println("worker1: ",message)
}
}
}
func Worker2(ctx context.Context,message string){
for ;;{
select{
case <-ctx.Done():{
fmt.Println("worker2 is cancled")
fmt.Println(ctx.Err()) //context canceled
return
}
default:
time.Sleep(2*1e9)
fmt.Println("worker2: ",message)
}
}
}
当到达设置的deadline后,该context和其子孙context都将主动的被取消。
3)WithTimeout
该函数的功能与WithDeadline基本相同,都使子context具备了一个自动取消时间,只不过接收的不是一个结束时间点,而是从当前函数调用开始,在取消之前的运行时间,因此接收的time.Duration类型,实际上,在底层实现上该函数就是直接调用了WithDeadline函数。
4)WIthValue
该函数使得生成子Context具备包含了一组key-value信息,该信息可以安全的被多个子Goroutine同步访问,虽然一个WithValue只能添加一组key-value信息,但其后续的子孙context再次使用WithValue添加的消息可以都共存,都可以通过函数value(key)查到,但如果存在相同key值的情况,老的k-v值将被覆盖(因为在底层实现上,父context将被包含在子context中)。
在源码中,设计者指出,为了避免与外部其他类型冲突,key的类型应该被定义成一个非外部的(开头字母小写)的类型。
使用实例如下:
package main
import (
"context"
"fmt"
"time"
)
type user struct{
name string
}
type key int
func main(){
u:=&user{"helloworld"}
var k key=1
ctx:=context.WithValue(context.Background(),k,u)
go middleContext(ctx)
time.Sleep(9*1e9)
}
func middleContext(ctx context.Context){
u:=&user{"withValueTest"}
var k key=2
newCtx:=context.WithValue(ctx,k,u)
go terminalContext(newCtx)
}
func terminalContext(ctx context.Context){
fmt.Println(ctx.Value(key(1)).(*user).name)//helloworld
fmt.Println(ctx.Value(key(2)).(*user).name)//withValueTest
}
WithValue并没有为context附加被主动取消的权力,得益于Context包良好的设计,我们可以灵活的使用这四个函数,达到根据需求对上下文状态的修改:
ctx,cancelFunc:=context.WithCancel(context.WithValue(context.Background(),k,u))
综上,结合源码作者注释的使用规则,总结Context包的使用方法如下:
第二部分链接:https://blog.csdn.net/qq_38093301/article/details/104378837