Golang并发模型之Context详解

简介:   对于 Golang 开发者来说context(上下文)包一定不会陌生。但很多时候,我们懒惰的只是见过它,或能起到什么作用,并不会去深究它。 应用场景:在 Go http 包的 Server 中,每一个请求在都有一个对应的goroutine去处理。

 

对于 Golang 开发者来说context(上下文)包一定不会陌生。但很多时候,我们懒惰的只是见过它,或能起到什么作用,并不会去深究它。

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

注意:go1.6及之前版本请使用golang.org/x/net/contextgo1.7及之后已移到标准库context

Context 原理

Context 的调用应该是链式的,通过WithCancelWithDeadlineWithTimeoutWithValue派生出新的 Context。当父 Context 被取消时,其派生的所有 Context 都将取消。

通过context.WithXXX都将返回新的 Context 和 CancelFunc。调用 CancelFunc 将取消子代,移除父代对子代的引用,并且停止所有定时器。未能调用 CancelFunc 将泄漏子代,直到父代被取消或定时器触发。go vet工具检查所有流程控制路径上使用 CancelFuncs。

遵循规则

遵循以下规则,以保持包之间的接口一致,并启用静态分析工具以检查上下文传播。

  1. 不要将 Contexts 放入结构体,相反context应该作为第一个参数传入,命名为ctx。 func DoSomething(ctx context.Context,arg Arg)error { // ... use ctx ... }
  2. 即使函数允许,也不要传入nil的 Context。如果不知道用哪种 Context,可以使用context.TODO()
  3. 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数
  4. 相同的 Context 可以传递给在不同的goroutine;Context 是并发安全的。

Context 包

Context 结构体。

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface { // Done returns a channel that is closed when this Context is canceled // or times out. Done() <-chan struct{} // Err indicates why this context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} } 
  • Done(),返回一个channel。当times out或者调用cancel方法时,将会close掉。
  • Err(),返回一个错误。该context为什么被取消掉。
  • Deadline(),返回截止时间和ok。
  • Value(),返回值。

所有方法

func Background() Context func TODO() Context func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context 

上面可以看到Context是一个接口,想要使用就得实现其方法。在context包内部已经为我们实现好了两个空的Context,可以通过调用Background()和TODO()方法获取。一般的将它们作为Context的根,往下派生。

WithCancel 例子

WithCancel 以一个新的 Done channel 返回一个父 Context 的拷贝。

   229  func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { 230 c := newCancelCtx(parent) 231 propagateCancel(parent, &c) 232 return &c, func() { c.cancel(true, Canceled) } 233 } 234 235 // newCancelCtx returns an initialized cancelCtx. 236 func newCancelCtx(parent Context) cancelCtx { 237 return cancelCtx{ 238 Context: parent, 239 done: make(chan struct{}), 240 } 241 } 

此示例演示使用一个可取消的上下文,以防止 goroutine 泄漏。示例函数结束时,defer 调用 cancel 方法,gen goroutine 将返回而不泄漏。

package main

import (
    "context"
    "fmt"
)

func main() { // gen generates integers in a separate goroutine and // sends them to the returned channel. // The callers of gen need to cancel the context once // they are done consuming generated integers not to leak // the internal goroutine started by gen. gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // returning not to leak the goroutine case dst <- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel when we are finished consuming integers for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } } 

WithDeadline 例子

   369  func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { 370 if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { 371 // The current deadline is already sooner than the new one. 372 return WithCancel(parent) 373 } 374 c := &timerCtx{ 375 cancelCtx: newCancelCtx(parent), 376 deadline: deadline, 377 } ...... 

可以清晰的看到,当派生出的子 Context 的deadline在父Context之后,直接返回了一个父Context的拷贝。故语义上等效为父。

WithDeadline 的最后期限调整为不晚于 d 返回父上下文的副本。如果父母的截止日期已经早于 d,WithDeadline (父,d) 是在语义上等效为父。返回的上下文完成的通道关闭的最后期限期满后,返回的取消函数调用时,或当父上下文完成的通道关闭,以先发生者为准。

看看官方例子:

package main

import (
    "context"
    "fmt" "time" ) func main() { d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) // Even though ctx will be expired, it is good practice to call its // cancelation function in any case. Failure to do so may keep the // context and its parent alive longer than necessary. defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) } } 

WithTimeout 例子

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。

   436  func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { 437 return WithDeadline(parent, time.Now().Add(timeout)) 438 } 

看看官方例子:

package main

import (
    "context"
    "fmt"
    "time" ) func main() { // Pass a context with a timeout to tell a blocking function that it // should abandon its work after the timeout elapses. ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) // prints "context deadline exceeded" } } 

WithValue 例子

   454  func WithValue(parent Context, key, val interface{}) Context { 454 if key == nil { 455 panic("nil key") 456 } 457 if !reflect.TypeOf(key).Comparable() { 458 panic("key is not comparable") 459 } 460 return &valueCtx{parent, key, val} 461 } 

WithValue 返回的父与键关联的值在 val 的副本。

使用上下文值仅为过渡进程和 Api 的请求范围的数据,而不是将可选参数传递给函数。

提供的键必须是可比性和应该不是字符串类型或任何其他内置的类型以避免包使用的上下文之间的碰撞。WithValue 用户应该定义自己的键的类型。为了避免分配分配给接口 {} 时,上下文键经常有具体类型结构 {}。另外,导出的上下文关键变量静态类型应该是一个指针或接口。

看看官方例子:

package main

import (
    "context"
    "fmt"
)

func main() { type favContextKey string 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")) } 

参考连接

[1] https://segmentfault.com/a/1190000006744213
[2] http://www.01happy.com/golang-context-reading/

 

 

 

我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。

谋胆并重
目录
相关文章
|
5天前
|
人工智能 Go 调度
掌握Go并发:Go语言并发编程深度解析
掌握Go并发:Go语言并发编程深度解析
|
5天前
|
安全 Java Go
Java vs. Go:并发之争
【4月更文挑战第20天】
25 1
|
5天前
|
Go 调度 开发者
CSP模型与Goroutine调度的协同作用:构建高效并发的Go语言世界
【2月更文挑战第17天】在Go语言的并发编程中,CSP模型与Goroutine调度机制相互协同,共同构建了高效并发的运行环境。CSP模型通过通道(channel)实现了进程间的通信与同步,而Goroutine调度机制则确保了并发任务的合理调度与执行。本文将深入探讨CSP模型与Goroutine调度的协同作用,分析它们如何共同促进Go语言并发性能的提升。
|
5天前
|
存储 算法 编译器
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
|
5天前
|
SQL Go 数据库
【Go语言专栏】Go语言中的事务处理与并发控制
【4月更文挑战第30天】Go语言在数据库编程中支持事务处理和并发控制,确保ACID属性和多用户环境下的数据完整性。`database/sql`包提供事务管理,如示例所示,通过`Begin()`、`Commit()`和`Rollback()`执行和控制事务。并发控制利用Mutex、WaitGroup和Channel防止数据冲突。结合事务与并发控制,开发者可处理复杂场景,实现高效、可靠的数据库应用。
|
5天前
|
Cloud Native Go 云计算
多范式编程语言Go:并发与静态类型的结合
Go语言是Google于2007年开发的开源编程语言,旨在提高程序开发和部署的效率。它的独特特征在于结合了并发处理与静态类型系统,提供了简洁、高效、并行处理能力的编程体验。本文将探讨Go语言的特点、应用场景以及其在现代软件开发中的优势。
|
5天前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
26 5
|
5天前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
35 4
|
5天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
35 4
|
5天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
26 2