Golang 并发 Context 的使用

Golang 并发 Context 的使用

Go在 1.7 引入了context包,目的是为了在不同的goroutine之间或跨API边界传递超时、取消信号和其他请求范围内的值(与该请求相关的值。这些值可能包括用户身份信息、请求处理日志、跟踪信息等等)。

Context 包的基本原理是通过在 goroutine 之间传递 Context 来实现请求范围数据、取消信号和截止时间的管理。当一个 goroutine 创建了一个新的 goroutine 时,它将 Context 作为参数传递给新的 goroutine。新的goroutine 可以使用这个 Context 来访问请求范围数据、接收取消信号和处理超时。

Go 语言中,Context 包是用于传递请求范围数据、取消信号和截至时间的机制。它通常被用来处理 goroutine 之间的通信和取消。Context 包是 Go 语言内置的,它可以很方便地使用,而不需要额外的依赖。

Context 包是一个轻量级的工具,它提供了一个标准的接口,用于在 goroutine 之间传递请求范围的数据、取消信号和截止时间。Context 包实现了一种类似于树状结构的数据结构,其中每个节点都表示一个请求范围。每个节点都有一个唯一的 key-value 对,其中 key 是一个 interface{} 类型的值,而 value 则是任何类型的值。Context 包还提供了一个可选的超时机制,用于在一定时间后自动取消请求。

如何使用

接口方法

type Context interface {
     Deadline() (deadline time.Time, ok bool)
     Done() <-chan struct{}
     Err() error
     Value(key interface{}) interface{}
 }

Context 接口中有4个方法:

ctx.Deadline() 方法返回截止时间和一个布尔值(指示截止时间是否已经设置),如果Context没有设置截止时间,该方法返回一个零值time.Time和一个布尔值false。

ctx.Done() 方法返回一个只读的 channel,当请求被取消或超时时,该 channel 将被关闭,多次调用 Done 方法会返回同一个 Channel。 可以通过监听这个通道来检测Context是否被取消。如果Context永不取消,则返回nil。

ctx.Value() 方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value并传入相同的Key会返回相同的结果,一般用于在Goroutine之间传递请求范围内的信息。如果没有关联的值,则返回nil。

ctx.Err() 方法返回一个错误,指示请求被取消的原因,分为3种情况

  • 如果Context尚未被取消,该方法返回
  • 如果当前 Context 被取消就会返回 context canceled 错误;
  • 如果当前 Context 超时就会返回 context deadline exceeded 错误。

Background 和 TODO

Background 和 TODO 函数返回一个非 nil 的空 Context,它没有携带任何的值,也没有取消和超时信号,虽然它的返回结果和函数一样,但是它们的使用场景是不一样的:

  • Background 通常作为根Context使用
  • 如果不确定使用哪个上下文时,可以使用 TODO
ctx1 := context.Background()
ctx2 := context.TODO()

WithCancel

WithCancel 接收一个父 Context,返回一个新的子 Context 和一个取消函数,当取消函数被调用时,子Context会被取消,同时会向子 Context关联的Done()通道发送取消信号,届时其衍生的子孙Context都会被取消。这个函数适用于手动取消操作的场景。

代码示例和场景查看: https://blog.csdn.net/qq_26857259/article/details/135979928

WithDeadline 和 WithTimeout

WithDeadline 函数接收一个父Context和一个截止时间作为参数,返回一个新的子Context。当截止时间到达时,子Context其衍生的子孙Context会被自动取消。这个函数适用于需要在特定时间点取消操作的场景。

package chapter02

import (
	"context"
	"fmt"
	"testing"
	"time"
)

func TestContextDeadLine(t *testing.T) {
	ctx := context.Background()

	deadCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*5))

	go func() {
		select {
		case <-deadCtx.Done():

			fmt.Println("child goroutine , ctx done .")
			fmt.Println(deadCtx.Err())
		case <-time.After(time.Second * 30):
			fmt.Println("child goroutine , timeout .")
		}
	}()

	fmt.Println("main goroutine , Before 10s , ing ...")
	time.Sleep(time.Second * 10)
	fmt.Println("main goroutine , After 10s , ing ...")
	time.Sleep(time.Second * 3)
	fmt.Println("main goroutine , After 3s , to cancel , end .")
	cancel()
}

输出

main goroutine , Before 10s , ing ...
child goroutine , ctx done .
context deadline exceeded
main goroutine , After 10s , ing ...
main goroutine , After 3s , to cancel , end .

上述示例中,用 WithDeadline 函数创建一个子 Context 对象 ctx 和一个取消函数 cancel,在一个 goroutine 中使用 select 语句监听 Context 对象的 Done 方法和 time.After 函数的返回值,

  • 如果 Done 方法返回一个非 nil 的 error,则说明 Context 已经被取消,
  • 否则说明 time.After 函数已经超时。

在主函数中,我们调用 cancel 函数来通知 Context 对象和其子 Context 对象,使它们都取消执行。最后,我们使用 time.Sleep 函数让程序等待一段时间,以便观察 Context 的执行情况。

WithTimeout

WithTimeout 函数和 WithDeadline 函数的功能是一样的,其底层会调用 WithDeadline 函数,只不过其第二个参数接收的是一个超时时间,而不是截止时间。这个函数适用于需要在一段时间后取消操作的场景。

源码如下所示:

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

WithValue

WithValue 函数可以用于创建一个 Context 对象,接收一个父 Context 和一个键值对key、val,返回一个新的子Context,并在其中添加一个key-value数据对。这个值可以是任意类型的数据,可以是基本类型、结构体或者指针等。需要注意的是,这个值只在当前 Context 对象及其子 Context 对象中有效,对于其他 Context 对象来说是不可见的。

func TestContextValue(t *testing.T) {
	ctx := context.Background()

	userKey := "key"

	ctxValue := context.WithValue(ctx, userKey, "value : [hello world !]")

	go func() {
		time.Sleep(time.Second * 3)
		fmt.Println("child goroutine , After 3s , to value ...")
		value := ctxValue.Value(userKey)
		fmt.Println("child goroutine , value : ", value)

	}()

	fmt.Println("main goroutine , Before 10s , ing ...")
	time.Sleep(time.Second * 10)
	fmt.Println("main goroutine , After 10s , end .")
}

在上面的代码中,使用 WithValue 函数创建一个子 Context 对象,并返回一个包含指定值的 Context 对象。接下来,我们在一个 goroutine 中使用 ctx.Value 函数获取 Context 对象中的值,并输出。在主函数中,让程序等待一段时间,以便观察 Context 的执行情况。

WithCancelCause 和 Cause

context.WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)函数是Go 1.20版本才新增的,其功能类似于context.WithCancel(),但是它可以设置额外的取消原因,也就是error信息,返回的cancel函数被调用时,需传入一个error 类型的参数。

ctx, cancelFunc := context.WithCancelCause(parentCtx)
defer cancelFunc(errors.New("原因"))

context.Cause(c Context) error函数用于返回取消Context的原因,即错误值error。如果是通过context.WithCancelCause()函数返回的取消函数cancelFunc(myErr)进行的取消操作,我们可以获取到myErr的值。否则,我们将得到与c.Err()相同的返回值。如果Context尚未被取消,将返回nil。

err := context.Cause(ctx)

Context 的使用场景

并发控制

一个典型的使用场景是,当我们需要启动多个 goroutine 进行任务处理时,我们可以使用 Context 来控制这些 goroutine 的执行。在每个 goroutine 中,我们都可以检测 Context 对象是否被取消,如果是,则退出 goroutine 的执行,否则继续执行。

超时控制

另一个典型的使用场景是,当我们需要对一个操作设置一个超时时间时,我们可以使用 Context 来控制这个操作的执行时间。在操作执行超时时,我们可以通知 Context 对象和其子 Context 对象取消执行。

gRPC 请求

在使用 gRPC 请求时,我们通常需要设置一个超时时间,以确保请求能够在规定的时间内得到响应。在这种情况下,我们可以使用 Context 来控制 gRPC 请求的执行时间。

HTTP 请求

在使用 HTTP 请求时,我们通常需要设置一个超时时间,以确保请求能够在规定的时间内得到响应。在这种情况下,我们可以使用 Context 来控制 HTTP 请求的执行时间。

总结

Context 包是 Go 语言中实现并发控制和超时控制的重要工具之一,可以帮助我们更加灵活的控制程序的执行。在实际应用中,我们可以使用 Context 包来实现一些复杂的功能,例如控制数据库连接池、处理 HTTP 请求和 gRPC 请求等。通过学习和使用 Context 包,我们可以更好地实现并发控制和超时控制,提高程序的可靠性和稳定性。

在使用 Context 包时,需要注意以下几点:

  1. Context 对象是线程安全的,可以被多个 goroutine 同时访问。
  2. 可以使用 WithCancel、WithDeadline 和 WithTimeout 函数来创建 Context 对象,并使用 context.Background 函数创建根 Context 对象。
  3. 在进行 goroutine 间的通信时,可以使用 Context 对象来传递消息和控制 goroutine 的执行。
  4. 在使用第三方库时,需要注意该库是否支持 Context 功能,以便正确的使用 Context 包。
  5. 在使用 Context 包时,需要避免出现 Context 滥用的情况,应该根据实际需要仅仅传递必要的 Context 对象,避免将 Context 对象传递到太多的函数中,导致程序难以维护和调试。

参考

  • https://zhuanlan.zhihu.com/p/628808975

你可能感兴趣的:(Golang,golang,开发语言)