• Go通关11:并发控制神器之Context深入浅出


    协程如何退出

    一个协程启动后,一般是代码执行完毕,自动退出,但是如果需要提前终止怎么办呢?
    一个办法是定义一个全局变量,协程中通过检查这个变量的变化来决定是否退出。这种办法须要加锁来保证并发安全,说到这里,有没有想的什么解决方案?
    select + channel 来实现:

    package main
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    func main() {
    	var wg sync.WaitGroup
    	stopWk := make(chan bool)
    	wg.Add(1)
    	go func() {
    		defer wg.Done()
    		worker(stopWk)
    	}()
    	time.Sleep(3*time.Second) //工作3秒
    	stopWk <- true //3秒后发出停止指令
    	wg.Wait()
    }
    
    func worker(stopWk chan bool){
    	for {
    		select {
    		case <- stopWk:
    			fmt.Println("下班咯~~~")
    			return
    		default:
    			fmt.Println("认真摸鱼中,请勿打扰...")
    		}
    		time.Sleep(1*time.Second)
    	}
    }
    

    运行结果:

    认真摸鱼中,请勿打扰...
    认真摸鱼中,请勿打扰...
    认真摸鱼中,请勿打扰...
    下班咯~~~
    

    可以看到,每秒打印一次“认真摸鱼中,请勿打扰...”,3秒后发出停止指令,程序进入 “下班咯~~~”。

    Context 初体验

    上面我们使用 select+channel 来实现了协程的终止,但是如果我们想要同时取消多个协程怎么办呢?如果需要定时取消又怎么办呢?
    此时,Context 就需要登场了,它可以跟踪每个协程,我们重写上面的示例:

    package main
    import (
    	"context"
    	"fmt"
    	"sync"
    	"time"
    )
    func main() {
    	var wg sync.WaitGroup
    	ctx, stop := context.WithCancel(context.Background())
    	wg.Add(1)
    	go func() {
    		defer wg.Done()
    		worker(ctx)
    	}()
    	time.Sleep(3*time.Second) //工作3秒
    	stop() //3秒后发出停止指令
    	wg.Wait()
    }
    
    func worker(ctx context.Context){
    	for {
    		select {
    		case <- ctx.Done():
    			fmt.Println("下班咯~~~")
    			return
    		default:
    			fmt.Println("认真摸鱼中,请勿打扰...")
    		}
    		time.Sleep(1*time.Second)
    	}
    }
    

    运行结果:

    认真摸鱼中,请勿打扰...
    认真摸鱼中,请勿打扰...
    认真摸鱼中,请勿打扰...
    下班咯~~~
    

    Context 介绍

    Context 是并发安全的,它是一个接口,可以手动、定时、超时发出取消信号、传值等功能,主要是用于控制多个协程之间的协作、取消操作。

    Context 接口有四个方法:

    type Context interface {
       Deadline() (deadline time.Time, ok bool)
       Done() <-chan struct{}
       Err() error
       Value(key interface{}) interface{}
    }
    
    • Deadline 方法:可以获取设置的截止时间,返回值 deadline 是截止时间,到了这个时间,Context 会自动发起取消请求,返回值 ok 表示是否设置了截止时间。
    • Done 方法:返回一个只读的 channel ,类型为 struct{}。如果这个 chan 可以读取,说明已经发出了取消信号,可以做清理操作,然后退出协程,释放资源。
    • Err 方法:返回Context 被取消的原因。
    • Value 方法:获取 Context 上绑定的值,是一个键值对,通过 key 来获取对应的值。

    最常用的是 Done 方法,在 Context 取消的时候,会关闭这个只读的 Channel,相当于发出了取消信号。

    Context 树

    我们并不需要自己去实现 Context 接口,Go 语言提供了函数来生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 就可以关联起来,父级 Context 发出取消信号,子级 Context 也会发出,这样就可以控制不同层级的协程退出。

    生成根节点

    1. emptyCtx是一个int类型的变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为 context 树的根节点。
    2. 但是我们一般不直接使用emptyCtx,而是使用由emptyCtx实例化的两个变量(background 、todo),分别通过调用BackgroundTODO方法得到,但这两个 context 在实现上是一样的。

    Background和TODO方法区别:
    BackgroundTODO只是用于不同场景下:Background通常被用于主函数、初始化以及测试中,作为一个顶层的context,也就是说一般我们创建的context都是基于Background;而TODO是在不确定使用什么context的时候才会使用。

    生成树的函数

    1. 可以通过 context。Background() 获取一个根节点 Context。
    2. 有了根节点后,再使用以下四个函数来生成 Context 树:
    • WithCancel(parent Context):生成一个可取消的 Context。
    • WithDeadline(parent Context, d time.Time):生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。
    • WithTimeout(parent Context, timeout time.Duration):生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消
    • WithValue(parent Context, key, val interface{}):生成一个可携带 key-value 键值对的 Context。

    Context 取消多个协程

    如果一个 Context 有子 Context,在该 Context 取消时,其下的所有子 Context 都会被取消。

    Context 传值

    Context 不仅可以发出取消信号,还可以传值,可以把它存储的值提供其他协程使用。

    示例:

    package main
    import (
    	"context"
    	"fmt"
    	"sync"
    	"time"
    )
    func main() {
    	var wg sync.WaitGroup
    	ctx, stop := context.WithCancel(context.Background())
    	valCtx := context.WithValue(ctx, "position","gopher")
    	wg.Add(2)
    	go func() {
    		defer wg.Done()
    		worker(valCtx, "打工人1")
    	}()
    	go func() {
    		defer wg.Done()
    		worker(valCtx, "打工人2")
    	}()
    	time.Sleep(3*time.Second) //工作3秒
    	stop() //3秒后发出停止指令
    	wg.Wait()
    }
    
    func worker(valCtx context.Context, name string){
    	for {
    		select {
    		case <- valCtx.Done():
    			fmt.Println("下班咯~~~")
    			return
    		default:
    			position := valCtx.Value("position")
    			fmt.Println(name,position, "认真摸鱼中,请勿打扰...")
    		}
    		time.Sleep(1*time.Second)
    	}
    }
    

    运行结果:

    打工人2 gopher 认真摸鱼中,请勿打扰...
    打工人1 gopher 认真摸鱼中,请勿打扰...
    打工人1 gopher 认真摸鱼中,请勿打扰...
    打工人2 gopher 认真摸鱼中,请勿打扰...
    打工人2 gopher 认真摸鱼中,请勿打扰...
    打工人1 gopher 认真摸鱼中,请勿打扰...
    下班咯~~~
    下班咯~~~
    

    Context 使用原则

    • Context 不要放在结构体中,需要以参数方式传递
    • Context 作为函数参数时,要放在第一位,作为第一个参数
    • 使用 context。Background 函数生成根节点的 Context
    • Context 要传值必要的值,不要什么都传
    • Context 是多协程安全的,可以在多个协程中使用
  • 相关阅读:
    第1课 Git、谁与争锋
    程序员最真实的10个瞬间
    程序员最真实的10个瞬间
    一文读懂前端缓存
    一文读懂前端缓存
    一文读懂前端缓存
    EF使用CodeFirst创建数据库和表
    EF使用CodeFirst创建数据库和表
    EF使用CodeFirst创建数据库和表
    ASP.NET MVC的过滤器笔记
  • 原文地址:https://www.cnblogs.com/isungge/p/15136715.html
Copyright © 2020-2023  润新知