• go context 包


    go context 包

    1 Context是什么

    1.1 介绍

    Context,翻译为"上下文",context包定义了Context接口类型,其接口方法定义了跨API和进程之间的执行最后期限、取消信号和其他请求范围的值

    并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作,他的底层是通过 channel 去传递管理信号。例如,父 goroutine 可以传递一个close信号来关闭此channel,子 goroutine 通过监听这个channel,那么子goroutine也会退出。

    context常用的使用场景:
    1. 一个请求对应多个goroutine之间的数据交互
    2. 超时控制
    3. 上下文控制

    1.2 Context接口方法

    context.Context是一个接口,该接口定义了四个需要实现的方法。具体如下:

    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其中:

    • Deadline:是获取设置的截止时间的意思,第一个返回值是截止时间,到了这个时间点,Context 会自动发起取消请求;第二个返回值 ok==false 时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
    • Done:该方法返回一个只读的 chan,类型为 struct{},我们在 goroutine 中,如果该方法返回的 chan 可以读取,则意味着parent context已经发起了取消请求,我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出 goroutine,释放资源。
    • Err 方法返回取消的错误原因,因为什么 Context 被取消。
    • Value方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 Key 才可以获取对应的值,这个值一般是线程安全的

    1.3 两个顶级Context

    context包提供两种顶级的上下文类型,这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象:

    func Background() Context

    context.Background()返回非零的空上下文。它从不被取消,没有值,也没有最后期限。它通常由主函数、初始化和测试使用,并且作为传入请求的顶级上下文。

    func TODO() Context

    context.TODO()返回非零的空上下文。当不清楚要使用哪个上下文或者它还不可用时,应该使用context.TODO()

    两者区别

    本质来讲两者区别不大,其源码实现是一样的,只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播

    1.4 派生Context(With系列函数)

    除以上两种顶级Context类型,context包提供四种创建可派生Context类型的函数

    WithCancel

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    
    • 1

    WithCancel函数返回具有新done通道的父级副本。当调用返回的cancel函数或关闭父上下文的done通道时(以先发生者为准),将关闭返回的上下文的done通道。
    取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。

    示例通过context控制多个协程停止:

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func task(ctx context.Context, s string) {
    lxx:
    	for {
    		select {
    		case <-ctx.Done():
    			fmt.Println("task:我收到取消指令,我结束了")
    			break lxx  // 结束掉 label位置的循环
    		default:
    			fmt.Println("打印一次传入的值:", s)
    			time.Sleep(1 * time.Second)
    
    		}
    	}
    }
    func main() {
    	parent := context.Background()
    	ctx, cancle := context.WithCancel(parent)
    	go task(ctx, "lxx is Nb")
    	go task(ctx, "lxx is handsome")
    	time.Sleep(5 * time.Second) // 睡个5s钟,发现上面两句话不停打印
    	cancle()  // 通过ctx控制,上面两个go协程关闭
    	time.Sleep(5 * time.Second)  // 睡个5s钟,发现确实被停止了,不打印了
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    WithDeadline

     func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    // 注意第二个参数是时间对象
    
    • 1
    • 2

    WithDeadline函数返回父上下文的副本,其截止时间调整为不迟于d。如果父上下文的截止时间早于d,则WithDeadline(Parent,d)在语义上等同于父上下文。当截止时间到期、调用返回的cancel函数或关闭父上下文的done通道(以先发生者为准)时,返回的上下文的done通道将关闭。
    取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。

    官方使用示例:
    这个例子传递一个具有任意截止时间的上下文,告诉一个阻塞函数一旦到达它就应该放弃它的工作。

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func task(ctx context.Context, s string) {
    lxx:
    	for {
    		select {
    		case <-ctx.Done():
    			fmt.Println("task:我收到取消指令,我结束了")
    			fmt.Println(ctx.Err())
    			// 正常到时间:context deadline exceeded
    			// 手动调用cancel :context canceled
    
    			break lxx // 结束掉 label位置的循环
    
    		default:
    			fmt.Println("打印一次传入的值:", s)
    			time.Sleep(1 * time.Second)
    
    		}
    	}
    }
    func task2(ctx context.Context, s string) {
    lxx:
    	for {
    		select {
    		case <-ctx.Done():
    			fmt.Println("task:我收到取消指令,我结束了")
    			fmt.Println(ctx.Err())
    			// 正常到时间:context deadline exceeded
    			// 手动调用cancel :context canceled
    
    			break lxx // 结束掉 label位置的循环
    		case <-time.After(1 * time.Second):
    			fmt.Println("1s时间到了,打印:",s)
    			fmt.Println(ctx.Err()) // 执行到此,如果还没到结束时间,Err为nil
    		}
    	}
    }
    func main() {
    	// 1 正常到时间
    	//parent := context.Background()
    	//t:=time.Now().Add(5*time.Second) // 5s后的时间
    	//ctx, _ := context.WithDeadline(parent,t)
    	//go task(ctx, "lxx is Nb")
    	//time.Sleep(10 * time.Second) // 睡个10s钟,由于5s结束,后5s没有输出
    
    	// 2 手动调用cancle取消
    	//parent := context.Background()
    	//t := time.Now().Add(5 * time.Second) // 5s后的时间
    	//ctx, cancel := context.WithDeadline(parent, t)
    	//go task(ctx, "lxx is Nb")
    	//time.Sleep(3 * time.Second) // 睡个3s钟,由于5s还没到,手动结束
    	//cancel()
    	//time.Sleep(7 * time.Second) // 再睡7s看输出
    
    	//3 1s后输出一次内容的另一种写法
    	parent := context.Background()
    	t := time.Now().Add(5 * time.Second) // 5s后的时间
    	ctx, cancel := context.WithDeadline(parent, t)
    	go task2(ctx, "lxx is Nb")
    	time.Sleep(3 * time.Second) // 睡个3s钟,由于5s还没到,手动结束
    	cancel()
    	time.Sleep(7 * time.Second) // 再睡7s看输出
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    WithTimeout

     func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    // 注意第二个参数是time.Duration 时间间隔
    
    • 1
    • 2

    WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消:

    WithTimeout WithDeadline 的区别在于 接收到参数类型不一样,一个接收的是 time.Duration 类型一个接收的是 time.Time 类型

    这个例子传递一个带有超时的上下文,告诉一个阻塞函数它应该在超时结束后放弃它的工作。

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func task(ctx context.Context) {
    	select {
    	case <-ctx.Done():
    		fmt.Println("task:我结束了")
    		// cancle函数取消会打印context canceled
    		// 到时间取消会打印:context deadline exceeded
    		fmt.Println(ctx.Err())
    	case <-time.After(1 * time.Second):
    		fmt.Println("1s时间到了")
    		fmt.Println(ctx.Err()) // 执行到此,如果还没到结束时间,Err为nil
    
    	}
    
    }
    
    func main() {
    	//ctx, cancle := context.WithTimeout(context.Background(), 1*time.Second) // 打印
    	ctx, cancle := context.WithTimeout(context.Background(), 2*time.Second)
    	go task(ctx)
    
    	time.Sleep(3*time.Second)
    	cancle()
    	time.Sleep(3*time.Second)
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    WithValue

     func WithValue(parent Context, key, val interface{}) Context
    
    • 1

    WithValue返回父级的副本,可为上下文设置一个键值对。
    只对传输进程和API的请求范围数据使用上下文值,而不用于向函数传递可选参数。
    提供的键必须是可比较的,并且不应是字符串或任何其他内置类型,以避免使用上下文的包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给接口时进行分配,上下文键通常具有具体的类型结构。或者,导出的上下文键变量的静态类型应该是指针或接口。

    示例:

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func task(ctx context.Context) {
    	fmt.Println(ctx.Value("name"))
    	select {
    	case <-ctx.Done():
    		fmt.Println("task:我结束了")
    		fmt.Println(ctx.Err())
    	case <-time.After(1 * time.Second):
    		fmt.Println("1s时间到了")
    		fmt.Println(ctx.Err())
    
    	}
    }
    
    func main() {
    	ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) // 1s超时的ctx
    	ctx = context.WithValue(ctx, "name", "lxx")
    	go task(ctx)
    	time.Sleep(5*time.Second)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2 Context使用示例

    2.1 控制10s后,所有协程退出

    使用context包来实现线程安全退出或超时的控制:控制10s后,所有协程退出

    package main
    
    import (
    	"context"
    	"fmt"
    	"strconv"
    	"sync"
    	"time"
    )
    
    func task(ctx context.Context, s string, wg *sync.WaitGroup) {
    	defer wg.Done()
    	for {
    		select {
    		case <-ctx.Done():
    			fmt.Println(s, "--->我结束了")
    			//fmt.Println(ctx.Err())
    			return
    		default:
    			fmt.Println(s)
    			time.Sleep(1 * time.Second)
    
    		}
    	}
    
    }
    
    func main() {
    	var wg sync.WaitGroup
    	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		s := fmt.Sprintf("我是第:%v 个任务", strconv.Itoa(i))
    		go task(ctx, s, &wg)
    	}
    	wg.Wait()
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    当并发体超时或main主动停止工作者Goroutine时,每个工作者都可以安全退出。

    2.2 控制某个go协程执行5次就结束

    // 控制goroutine 执行5次结束
    func main() {
    	// 定义一个运行次数变量
    	runCount := 0
    	//定义一个waitgroup,等待goroutine执行完成
    	var wg sync.WaitGroup
    	// 初始化context
    	parent := context.Background()
    	// 传入初始化的ctx,返回ctx和cancle函数
    	ctx, cancle := context.WithCancel(parent)
    	wg.Add(1) // 增加一个任务
    	go func() {
    		for {
    			select {
    			case <-ctx.Done():
    				fmt.Println("任务结束")
    				return
    			default:
    				fmt.Printf("任务执行了%d次\n", runCount)
    				runCount++
    			}
    			// 执行了5次,使用ctx的取消函数将任务取消
    			if runCount >= 5 {
    				cancle()
    				wg.Done() // goroutine执行完成
    			}
    
    		}
    	}()
    
    	wg.Wait() //等待所有任务完成
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    2.3 打印100个素数

    Go语言是带内存自动回收特性的,因此内存一般不会泄漏。当main函数不再使用管道时后台Goroutine有泄漏的风险。我们可以通过context包来避免这个问题,下面是防止内存泄露的素数筛实现:

    // 返回生成自然数序列的管道: 2, 3, 4, ...
    func GenerateNatural(ctx context.Context) chan int {
        ch := make(chan int)
        go func() {
            for i := 2; ; i++ {
                select {
                //父协程cancel()时安全退出该子协程
                case <- ctx.Done():
                    return
                //生成的素数发送到管道
                case ch <- i:
                }
            }
        }()
        return ch
    }
    
    // 管道过滤器: 删除能被素数整除的数
    func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
        out := make(chan int)
        go func() {
            for {
                if i := <-in; i%prime != 0 {
                    select {
                    //父协程cancel()时安全退出该子协程
                    case <- ctx.Done():
                        return
                    case out <- i:
                    }
                }
            }
        }()
        return out
    }
    
    func main() {
        // 使用一个可由父协程控制子协程安全退出的Context。
        ctx, cancel := context.WithCancel(context.Background())
    
        ch := GenerateNatural(ctx) // 自然数序列: 2, 3, 4, ...
        
        for i := 0; i < 100; i++ {
            // 新出现的素数打印出来
            prime := <-ch 
            fmt.Printf("%v: %v\n", i+1, prime)
            // 基于新素数构造的过滤器
            ch = PrimeFilter(ctx, ch, prime) 
        }
        
        //输出100以内符合要求的素数后安全退出所有子协程
        cancel()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    当main函数完成工作前,通过调用cancel()来通知后台Goroutine退出,这样就避免了Goroutine的泄漏。
    在这里插入图片描述

    3 使用Context的注意事项

    • 推荐以参数的方式显示传递Context
    • 以Context作为参数的函数方法,应该把Context作为第一个参数。
    • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什 么,就使用context.TODO()
    • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
    • Context是线程安全的,可以放心的在多个goroutine中传递
  • 相关阅读:
    Java多线程基础知识-2
    5. 树的存储结构:【双亲表示法、孩子表示法、孩子兄弟表示法】+【树、森林和二叉树的转换】
    LeetCode C++ 88.合并两个有序数组
    lio-sam框架:回环检测及位姿计算
    java spring cloud 企业工程管理系统源码+二次开发+定制化服务
    java毕业设计MVC的时鲜蔬菜配送系统Mybatis+系统+数据库+调试部署
    关于 SAP ABAP CL_HTTP_CLIENT API 中的 SSL_ID 参数
    buildAdmin 后端控制器的代码分析
    vue2相关
    基于java+springboot+mybatis+vue+elementui的勤工助学管理系统
  • 原文地址:https://blog.csdn.net/qq_55752792/article/details/126122548