Golang问题点(三) - Context的问题点

Go的标准库 context

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

那么如何控制goroutine优雅的退出呢?

1. 实例

1.0. 基础代码

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func DoSomething() {
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
    }
    wg.Done()
}

func main() {
    wg.Add(1)
  go DoSomething()  //   这个DoSomething() 没有停止条件,不会停止
    wg.Wait()
    fmt.Println("DoSomething over")
}

1. 1全局变量方式

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var status bool = true

func DoSomething() {
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        if !status {
            break
        } else {
            continue
        }
    }
    wg.Done()
}

func main() {
    wg.Add(1)
    go DoSomething()
    time.Sleep(10 * time.Second)
    status = false  //通过修改全局变量的状态,给gorotine发送终止信号
    wg.Wait()
    fmt.Println("DoSomething over")
}

1.2. select + channel 版本

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var chann = make(chan bool)

func DoSomething() {
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        select {
        case tag := <-chann: // 信号接收
            if !tag {
                wg.Done()
                return
            }
        default:
            continue
        }
    }
}

func main() {
    wg.Add(1)
    go DoSomething()
    time.Sleep(5 * time.Second)
    chann <- false // 通过无缓存的channel来给goroutine发送终止信号
    wg.Wait()
    fmt.Println("DoSomething over")
}

1.3.context版本

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)
var wg sync.WaitGroup

func DoSomething(ctx context.Context) {

    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        select {
        case <-ctx.Done(): // 等待父级通知
            wg.Done()
            return
        default:
            continue
        }
    }

}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go DoSomething(ctx)
    time.Sleep(10 * time.Second)
    cancel() //cancel()执行,通知子goroutine结束
    wg.Wait()
    fmt.Println("DoSomething over")
}

当在DoSomething中再开启子goroutine时,将ctx传入到函数当中即可:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func DoSomething2(ctx context.Context) {
    for {
        fmt.Println("董小小贱")
        time.Sleep(2 * time.Second)
        select {
        case <-ctx.Done():
            wg.Done()
            return
        default:
            continue
        }
    }

}

func DoSomething(ctx context.Context) {
    go DoSomething2(ctx)
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        select {
        case <-ctx.Done():
            wg.Done()
            return
        default:
            continue
        }
    }

}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(2)
    go DoSomething(ctx)
    time.Sleep(10 * time.Second)
    cancel()
    wg.Wait()
    fmt.Println("DoSomething over")
}

那么context的作用就呼之欲出了:简单讲就是用来处理goroutine之间传递截止日期、取消信号和其他请求范围内的值

2. context相关


Go1.7 加入的新的标准库context 它定义了Context类型, 它跨API边界和进程之间传递截止日期、取消信号和其他请求范围内的值。

对服务器的传入请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传播上下文,可以选择将其替换为使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,从它派生的所有上下文也被取消。

WithCancelWithDeadlineWithTimeout函数接收一个Context并返回一个派生子Context和一个CancelFunc。调用CancelFunc取消子进程及其子进程,删除父进程对子进程的引用,并停止所有相关的计时器。如果没有调用CancelFunc,则会泄漏子节点及其子节点,直到父节点被取消或计时器触发。

不要将Context存储在结构类型中,应该显示地将上下文传递给需要它的每个函数,Context应该是第一个参数,通常命名为ctx。


2.1 context包相关

2.1.1 context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}


// Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
//Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;
//Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;
        //如果当前Context被取消就会返回Canceled错误;
        //如果当前Context超时就会返回DeadlineExceeded错误;
//Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
2.1.2 CancelFunc
type CancelFunc func()

// CancelFunc告诉一个goroutine放弃它的工作。CancelFunc不会等待工作停止。在第一次调用之后,对CancelFunc的后续调用什么也不做。
2.1.3 Background()
func Background() Context

// Background返回一个非nil的空上下文。它从未被取消,没有值,也没有期限。它通常由主函数、初始化和测试使用,并作为传入请求的根context(最顶层的context,其他的都是它的派生子context)
2.1.4 TODO()
func TODO() Context

//TODO返回一个非nil的空context。代码应该使用context。TODO当不清楚要使用哪个context或者它还不可用时(因为周围的函数还没有被扩展到接受context参数)。TODO由静态分析工具识别,该工具确定context是否在程序中正确传播

//这个就是知道要用context,但不清楚用哪个,埋点用。
backgroundtodo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

2.2 With系列函数

2.2.1 WithCancel
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

//WithCancel返回带有新Done通道的父进程的一个副本。当返回的cancel函数被调用时,或者当父context的Done通道被关闭时,返回context的Done通道将被关闭,以最先发生的情况为准。

// 取消此context释放与之关联的资源,因此代码应该在此context中运行的操作完成后立即调用cancel。

具体用法可以看上边的例子。

2.2.2 WithDeadline
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

// deadline 是绝对时间,比如:d := time.Now().Add(50 * time.Millisecond)

//WithDeadline返回父context的一个副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。返回的上下文的Done通道在deadline过期时关闭,在调用返回的cancel函数时关闭,或者在关闭父上下文的Done通道时关闭,以最先发生的情况为准。

// 取消此上下文释放与之关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

一个例子:

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)
    defer cancel()
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}


// 这里会先执行ctx.Done() 然后打印异常
2.2.1 WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

// 跟WithDeadline的区别在于,WithTimeout是用的相对时间。
2.2.1 WithValue
func WithValue(parent Context, key, val interface{}) Context

//WithValue返回父节点的一个副本,其中与键关联的值为val。

//只将上下文值用于传输流程和api的请求范围的数据,而不用于将可选参数传递给函数。

// 提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用context的包之间的冲突。WithValue的用户应该定义自己的键类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

一个例子:

package main

import (
    "context"
    "fmt"
)

type favContextKey string

func main() {

    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }

    k := favContextKey("language")
    ctx := context.WithValue(context.Background(), k, "Go")

    f(ctx, k)
    f(ctx, favContextKey("color"))
}

2.3 context的注意事项:

- 推荐以参数的方式显示传递Context
- 以Context作为参数的函数方法,应该把Context作为第一个参数,显示传递。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
- Context是线程安全的,可以放心的在多个goroutine中传递
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355