zoukankan      html  css  js  c++  java
  • Golang Context 包详解

    Golang Context 包详解

    0. 引言

    在 Go 语言编写的服务器程序中,服务器通常要为每个 HTTP 请求创建一个 goroutine 以并发地处理业务。同时,这个 goroutine 也可能会创建更多的 goroutine 来访问数据库或者 RPC 服务。当这个请求超时或者被终止的时候,需要优雅地退出所有衍生的 goroutine,并释放资源。因此,我们需要一种机制来通知衍生 goroutine 请求已被取消。 比如以下例子,sleepRandom_1 的结束就无法通知到 sleepRandom_2。

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func sleepRandom_1() {
    	i := 0
    	for {
    		time.Sleep(1 * time.Second)
    		fmt.Printf("This is sleep Random 1: %d
    ", i)
    
    		i++
    		if i == 5 {
    			fmt.Println("cancel sleep random 1")
    			break
    		}
    	}
    }
    
    func sleepRandom_2() {
    	i := 0
    	for {
    		time.Sleep(1 * time.Second)
    		fmt.Printf("This is sleep Random 2: %d
    ", i)
    		i++
    	}
    }
    
    func main() {
    
    	go sleepRandom_1() // 循环 5 次后退出
    	go sleepRandom_2() // 会一直打印 This is sleep Random 2
    
    	for {
    		time.Sleep(1 * time.Second)
    		fmt.Println("Continue...")
    	}
    }
    

    1. Context

    Context 包提供上下文机制在 goroutine 之间传递 deadline、取消信号(cancellation signals)或者其他请求相关的信息。使用方法是:

    1. 首先,服务器程序为每个接受的请求创建一个 Context 实例(称为根 context,通过 context.Background() 方法创建);
    2. 之后的 goroutine 接受根 context 的一个派生 Context 对象。比如通过调用根 context 的 WithCancel 方法,创建子 context;
    3. goroutine 通过 context.Done() 方法监听取消信号。func Done() <-chan struct{} 是一个通信操作,会阻塞 goroutine,直到收到取消信号接触阻塞。
      (可以借助 select 语句,如果收到取消信号,就退出 goroutine;否则,默认子句是继续执行 goroutine);
    4. 当一个 Context 被取消(比如执行了 cancelFunc()),那么该 context 派生出来的 context 也会被取消。

    1.1 Context 类型

    // A Context carries a deadline, a cancelation signal, and other values across
    // API boundaries.
    //
    // Context's methods may be called by multiple goroutines simultaneously.
    type Context interface {
    
        Done() <-chan struct{}
    
        Deadline() (deadline time.Time, ok bool)
        
        Err() error
        
        Value(key interface{}) interface{}
    }
    

    Done() <-chan struct{}

    Done 方法返回一个单向只读 channel。调用 Done() 会阻塞当前运行的代码,直到以下条件之一发生时,channel 才会被关闭、解除阻塞:

    1. WithCancel 创建的 context,cancelFunc 被调用。该 context 以及派生子 context 的 Done channel 都会收到取消信号;
    2. WithDeadline 创建的 context,deadline 到期。
    3. WithTimeout 创建的 context,timeout 到期

    Done 要配合 select 语句使用:

    // DoSomething 生产数据并发送给通道 out
    // 但如果 DoSomething 返回一个则退出函数,
    // 或者 ctx.Done 被关闭时也会退出函数.
    func Stream(ctx context.Context, out chan<- Value) error {
    	for {
    		v, err := DoSomething(ctx)
    		if err != nil {
    			return err
    		}
    		select {
    		case <-ctx.Done():
    			return ctx.Err()
    		case out <- v:
    		}
    	}
    }
    

    Deadline() (deadline time.Time, ok bool)

    WithDeadline 方法会给 context 设置 deadline,到期自动发送取消信号。调用 Deadline() 返回 deadline 的值。如果没设置,ok 返回 false。
    该方法可用于确定当前时间是否临近 deadline。

    Err() error

    如果 Done 的 channel 被关闭了, Err 函数会返回一个 error,说明错误原因:

    1. 如果 channel 是因为被取消而关闭,打印 canceled;
    2. 如果 channel 是因为 deadline 到时了,打印 deadline exceeded。

    重复调用,返回相同值。

    Value(key interface{}) interface{}

    返回由 WithValue 关联到 context 的值。

    1.2 创建根 Context

    有两种方法创建根 Context:

    1. context.Background()
    2. context.TODO()

    根 context 不会被 cancel。这两个方法只能用在最外层代码中,比如 main 函数里。一般使用 Background() 方法创建根 context。
    TODO() 用于当前不确定使用何种 context,留待以后调整。

    1.3 派生 Context

    一个 Context 被 cancel,那么它的派生 context 都会收到取消信号(表现为 context.Done() 返回的 channel 收到值)。
    有四种方法派生 context :

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

    2. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

    3. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

    4. func WithValue(parent Context, key, val interface{}) Context

    WithCancel

    最常用的派生 context 方法。该方法接受一个父 context。父 context 可以是一个 background context 或其他 context。
    返回的 cancelFunc,如果被调用,会导致 Done channel 关闭。因此,绝不要把 cancelFunc 传给其他方法。

    WithDeadline

    该方法会创建一个带有 deadline 的 context。当 deadline 到期后,该 context 以及该 context 的可能子 context 会受到 cancel 通知。另外,如果 deadline 前调用 cancelFunc 则会提前发送取消通知。

    WithTimeout

    与 WithDeadline 类似。创建一个带有超时机制的 context。

    WithValue

    WithValue 方法创建一个携带信息的 context,可以是 user 信息、认证 token等。该 context 与其派生的子 context 都会携带这些信息。

    WithValue 方法的第二个参数是信息的唯一 key。该 key 类型不应对外暴露,为了避免与其他包可能的 key 类型冲突。所以使用 WithValue 也
    应像下面例子的方式间接调用 WithValue。

    WithValue 方法的第三个参数即是真正要存到 context 中的值。

    使用 WithValue 的例子:

    package user
    
    import "context"
    
    // User 类型对象会被保存到 Context 中
    type User struct {
        // ...
    }
    
    // key 不应该暴露出来。这样避免与包中其他 key 类型冲突
    type key int
    
    // userKey 是 user 的 key,不应暴露; 
    // 通过 user.NewContext 和 user.FromContext 间接使用 key
    var userKey key
    
    // NewContext 返回携带 u 作为 value 的 Context
    func NewContext(ctx context.Context, u *User) context.Context {
    	return context.WithValue(ctx, userKey, u)
    }
    
    // FromContext 返回关联到 context 的 User类型的 value 的值
    func FromContext(ctx context.Context) (*User, bool) {
    	u, ok := ctx.Value(userKey).(*User)
    	return u, ok
    }
    

    2. 例子

    改进引子里的例子。 sleepRandom_1 结束后,会触发 cancelParent() 被调用。所以 sleepRandom_2 中的 ctx.Done() 会被关闭。sleepRandom_2 执行退出。

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func sleepRandom_1(stopChan chan struct{}) {
    	i := 0
    	for {
    		time.Sleep(1 * time.Second)
    		fmt.Printf("This is sleep Random 1: %d
    ", i)
    
    		i++
    		if i == 5 {
    			fmt.Println("cancel sleep random 1")
    			stopChan <- struct{}{}
    			break
    		}
    	}
    }
    
    func sleepRandom_2(ctx context.Context) {
    	i := 0
    	for {
    		time.Sleep(1 * time.Second)
    		fmt.Printf("This is sleep Random 2: %d
    ", i)
    		i++
    
    		select {
    		case <-ctx.Done():
    			fmt.Printf("Why? %s
    ", ctx.Err())
    			fmt.Println("cancel sleep random 2")
    			return
    		default:
    		}
    	}
    }
    
    func main() {
        
        ctxParent, cancelParent := context.WithCancel(context.Background())
        ctxChild, _ := context.WithCancel(ctxParent)
        
        stopChan := make(chan struct{})
    
        go sleepRandom_1(stopChan)
        go sleepRandom_2(ctxChild)
    
        select {
        case <- stopChan:
            fmt.Println("stopChan received")
        }
        cancelParent()
        
        for {
            time.Sleep(1 * time.Second)
            fmt.Println("Continue...")
        }
    }
    

    3. 参考文档

    Go Concurrency Patterns: Context

    Understanding the context package in golang

  • 相关阅读:
    linux 内核定时器 timer_list详解
    linux2.6源码分析之解压内核映像 head.s
    [C#]我自己写的一个对字节中每位进行修改值的函数
    Android Intent调用大全
    proguard 原理
    何为夫妻?何为家?何为幸福?
    生命只是瞬间,而有些人终究是过客?(转)
    bind端口复用
    在android开发中应该如何管理内存或者是在开发过程中应该注意哪些问题来较少OOM?
    W/ActivityManager( 1419): Activity is launching as a new task, so cancelling activity result.
  • 原文地址:https://www.cnblogs.com/huanggze/p/11296822.html
Copyright © 2011-2022 走看看