【每天一个Go知识点】(9) go: context(上下文)使用

版权声明:本文为CSDN博主「Word哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/finghting321/article/details/106012673/
————————————————

1. 为什么需要context

在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。

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

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

context.Context接口:

type Context interface {
    // 返回Context的超时时间(超时返回场景)
    Deadline() (deadline time.Time, ok bool)
 
    // 在Context超时或取消时(即结束了)返回一个关闭的channel
    // 即如果当前Context超时或取消时,Done方法会返回一个channel,然后其他地方就可以通过判断Done方法是否有返回(channel),如果有则说明Context已结束
    // 故其可以作为广播通知其他相关方本Context已结束,请做相关处理。
    Done() <-chan struct{}
 
    // 返回Context取消的原因
    Err() error
    
    // 返回Context相关数据
    Value(key interface{}) interface{}
}

继承的Context,BackGound是所有Context的root,不能够被cancel。context包提供了三种context,分别是是普通context,超时context以及带值的context:

// 普通context,通常这样调用: ctx, cancel := context.WithCancel(context.Background())
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
 
// 带超时的context,超时之后会自动close对象的Done,与调用CancelFunc的效果一样
// WithDeadline 明确地设置一个d指定的系统时钟时间,如果超过就触发超时
// WithTimeout 设置一个相对的超时时间,也就是deadline设为timeout加上当前的系统时间
// 因为两者事实上都依赖于系统时钟,所以可能存在微小的误差,所以官方不推荐把超时间隔设置得太小
// 通常这样调用:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
 
// 带有值的context,没有CancelFunc,所以它只用于值的多goroutine传递和共享
// 通常这样调用:ctx := context.WithValue(context.Background(), "key", myValue)
func WithValue(parent Context, key, val interface{}) Context
3. 场景举例—等待组
package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
//数据接收服务主协程同子协程同步变量
var wg sync.WaitGroup
 
func run(i int) {
    fmt.Println("start 任务ID:", i)
    time.Sleep(time.Second * 1)
    wg.Done() // 每个goroutine运行完毕后就释放等待组的计数器
}
 
func main() {
    countThread := 2 //runtime.NumCPU()
    for i := 0; i < countThread; i++ {
        go run(i)
    }
    wg.Add(countThread) // 需要开启的goroutine等待组的计数器
 
    //等待所有的任务都释放
    wg.Wait()
    fmt.Println("任务全部结束,退出")
}

运行结果:


分析:对于等待组控制多并发的情况,只有所有的goroutine都结束了才算结束,只要有一个goroutine没有结束, 那么就会一直等,这显然对资源的释放是缓慢的;
优点:使用等待组的并发控制模型,适用于好多个goroutine协同做一件事情,因为每个goroutine做的都是这件事情的一部分,只有当全部的goroutine都完成,这件事情才算完成;
缺点:需要主动的通知某一个 goroutine 结束。
疑问:如果开启一个后台 goroutine 一直做事情,现在不需要了,那么就需要通知这个goroutine 结束,否则它会一直跑。

4. 场景举例—通道+select

针对等待组场景遗留的问题,解决办法:

  1. 设置全局变量,在通知goroutine要停止时,为全局变量赋值,但是这样必须保证线程安全,不可避免的必须为全局变量加锁,显得有失便利;
  2. 使用chan + select多路复用的方式,就会优雅许多;
package main
 
import (
    "fmt"
    "time"
)
 
func run(stop chan bool) {
    for {
        select {
        case <-stop:
            fmt.Println("任务1结束退出")
            return
        default:
            fmt.Println("任务1正在运行中")
            time.Sleep(time.Second * 2)
        }
    }
}
 
func main() {
    stop := make(chan bool)
    go run(stop) // 开启goroutine
 
    // 运行一段时间后停止
    time.Sleep(time.Second * 10)
    fmt.Println("停止任务1。。。")
    stop <- true
    time.Sleep(time.Second * 3)
    return
}

运行结果:

优点:优雅、简单
不足:如果有很多 goroutine 都需要控制结束,并且这些 goroutine 又开启其它更多的goroutine ?

5. 场景举例—普通context
package main
 
import (
    "context"
    "fmt"
    "time"
)
 
func run(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("任务%v结束退出\n", id)
            return
        default:
            fmt.Printf("任务%v正在运行中\n", id)
            time.Sleep(time.Second * 2)
        }
    }
}
 
func main() {
    //管理启动的协程
    ctx, cancel := context.WithCancel(context.Background())
    // 开启多个goroutine,传入ctx
    go run(ctx, 1)
    go run(ctx, 2)
    // 运行一段时间后停止
    time.Sleep(time.Second * 10)
    fmt.Println("停止任务1")
    cancel() // 使用context的cancel函数停止goroutine
    // 为了检测监控过是否停止,如果没有监控输出,表示停止
    time.Sleep(time.Second * 3)
    return
}

说明:context.Background() 返回一个空的 Context,这个空的 Context 一般用于整个 Context 树的根节点。然后使用 context.WithCancel(parent) 函数,创建一个可取消的子 Context,然后当作参数传给 goroutine 使用,这样就可以使用这个子 Context 跟踪这个 goroutine。

运行结果:

6. 场景举例—Context超时
package main
 
import (
    "context"
    "fmt"
    "sync"
    "time"
)
 
func coroutine(ctx context.Context, duration time.Duration, id int, wg *sync.WaitGroup) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("协程 %d 退出\n", id)
            wg.Done()
            return
        case <-time.After(duration):
            fmt.Printf("消息来自协程 %d\n", id)
        }
    }
}
 
func main() {
    //使用WaitGroup等待所有的goroutine执行完毕,在收到<-ctx.Done()的终止信号后使wg中需要等待的goroutine数量减一。
    // 因为context只负责取消goroutine,不负责等待goroutine运行,所以需要配合一点辅助手段
    //管理启动的协程
    wg := &sync.WaitGroup{}
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go coroutine(ctx, 1*time.Second, i, wg)
    }
    wg.Wait()

说明:代码中使用WaitGroup等待所有的goroutine执行完毕,在收到<-ctx.Done()的终止信号后使wg中需要等待的goroutine数量减一, 因为context只负责取消goroutine,不负责等待goroutine运行,需要配合一点辅助手段
运行结果:

7. 场景举例—Context传递元数据
package main
 
import (
    "context"
    "fmt"
    "time"
)
 
var key string = "name"
 
func run(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("任务%v结束退出\n", ctx.Value(key))
            return
        default:
            fmt.Printf("任务%v正在运行中\n", ctx.Value(key))
            time.Sleep(time.Second * 2)
        }
    }
}
 
func main() {
    //管理启动的协程
    ctx, cancel := context.WithCancel(context.Background())
    // 给ctx绑定键值,传递给goroutine
    valuectx := context.WithValue(ctx, key, "【监控1】")
    // 开启goroutine,传入ctx
    go run(valuectx)
    // 运行一段时间后停止
    time.Sleep(time.Second * 10)
    fmt.Println("停止任务")
    cancel() // 使用context的cancel函数停止goroutine
    // 为了检测监控过是否停止,如果没有监控输出,表示停止
    time.Sleep(time.Second * 3)
}

运行结果:

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

推荐阅读更多精彩内容