您的位置:首页 > 教程 > 其他脚本 > Go语言并发编程基础上下文概念详解

Go语言并发编程基础上下文概念详解

2022-08-05 17:45:54 来源:易采站长站 作者:

Go语言并发编程基础上下文概念详解

目录
前言1 Go 中的 Context2 Context 接口3 Context Tree4 创建上下文4.1 上下文创建函数4.2 Context 使用规范4.3 Context 使用场景5 总结

前言

相信大家以前在做阅读理解的时候,一定有从老师那里学一个技巧或者从参考答案看个:结合上下文。根据上下文我们能够找到有助于解题的相关信息,也能更加了解段落的思想。

在开发过程中,也有这个上下文(Context)的概念,而且上下文也必不可少,缺少上下文,就不能获取完整的程序信息。那么什么是程序中的上下文呢?

简单来说,就是在>

1>

Golang 的上下文也是应用开发常用的并发控制工具。同理,上下文可以用于在程序中的 API 层或进程之间共享请求范围的数据,除此之外,Go 的 Context 库还提供取消信号(Cancel)以及超时机制(Timeout)。

Context 又被称为上下文,与 WaitGroup 不同的是,Context 对于派生 goroutine 有更强的控制力,可以管理多级的 goroutine。

但我们在 Go 中创建一个 goroutine 时,如果发生了一个错误,并且这个错误永远不会终止,而其他程序会继续进行。加入有一个不被调用的 goroutine 运行无限循环,如下所示:

package main
import "fmt"
func main() {
    dataCom := []string{"alex", "kyrie", "kobe"}
    go func(data []string) {
        // 模拟大量运算的死循环
    }(dataCom)
    // 其他代码正常执行
    fmt.Println("剩下的代码执行正常逻辑")
}

上面的例子并不完整,dataCom goroutine 可能会也可能不会成功处理数据。它可能会进入无限循环或导致错误。我们的其余代码将不知道发生了什么。

有多种方法可以解决这个问题。其中之一是使用通道向我们的主线程发送一个信号,表明这个 goroutine 花费的时间太长,应该取消它。

package main
import (
	"fmt"
	"time"
)
func main() {
	stopChannel := make(chan bool)
	dataCom := []string{"alex", "kyrie", "kobe"}
	go func(stopChannel chan bool) {
		go func(data []string) {
			// 大量的计算
		}(dataCom)
		for range time.After(2 * time.Second) {
			fmt.Println("此操作运行时间过长,取消中")
			stopChannel <- true
		}
	}(stopChannel)
	<-stopChannel
	// 其他代码正常执行
	fmt.Println("剩下的代码执行正常逻辑")
}

上面的逻辑很简单。我们正在使用一个通道向我们的主线程发出这个 goroutine 花费的时间太长的信号。但是同样的事情可以用 context 来完成,这正是 context 包存在的原因。

package main
import (
	"context"
	"fmt"
	"time"
)
func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
	defer cancel()
	dataCom := []string{"alex", "kyrie", "kobe"}
	go func() {
		go func(data []string) {
			// 大量的计算
		}(dataCom)
		for range time.After(2 * time.Second) {
			fmt.Println("此操作运行时间过长,取消中")
			cancel()
			return
		}
	}()
	select {
	case <-ctx.Done():
		fmt.Println("上下文被取消")
	}
}

2>

Context 接口定义:

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

Context 接口定义了 4 个方法:

    Deadline(): 返回取消此上下文的时间 deadline(如果有)。如果未设置 deadline 时,则返回 ok==false,此时 deadline 为一个初始值的 time.Time 值。后续每次调用这个对象的 Deadline 方法时,都会返回和第一次调用相同的结果。Done() : 返回一个用于探测 Context 是否取消的 channel,当 Context 取消会自动将该 channel 关闭,如果该 Context 不能被永久取消,该函数返回 nil。例如 context.Background();如果 Done 被 close,Err 方法会返回 Done 被 close 的原因。Err(): 该方法会返回 context 被关闭的原因,关闭原因由 context 实现控制,不需要用户设置;如果 Done() 尚未关闭,则 Err() 返回 nilValue() : 在树状分布的 goroutine 之间共享数据,用 map 键值的工作方法,通过 key 值查询 value。

    每次创建新上下文时,都会得到一个符合此接口的类型。上下文的真正实现隐藏在这个包和这个接口后面。这些是您可以创建的工厂类型的上下文:

    context.TODO

    context.Background

    context.WithCancel

    context.WithValue

    context.WithTimeout

    context.WithDeadline

    3>

    在实际实现中,我们通常使用派生上下文。我们创建一个父上下文并将其传递到一个层,我们派生一个新的上下文,它添加一些额外的信息并将其再次传递到下一层,依此类推。通过这种方式,我们创建了一个从作为父级的根上下文开始的上下文树。

    这种结构的优点是我们可以一次性控制所有上下文的取消。如果根信号关闭了上下文,它将在所有派生的上下文中传播,这些上下文可用于终止所有进程,立即释放所有内容。这使得上下文成为并发编程中非常强大的工具。

    4>

    4.1>

    我们可以从现有的上下文中创建或派生上下文。顶层(根)上下文是使用 BackgroundTODO 方法创建的,而派生上下文是使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 方法创建的。

    所有派生的上下文方法都返回一个取消函数 CancelFunc,但 WithValue 除外,因为它与取消无关。调用 CancelFunc 会取消子项及其子项,删除父项对子项的引用,并停止任何关联的计时器。调用 CancelFunc 失败会泄漏子项及其子项,直到父项被取消或计时器触发。

      context.Background() ctx Context

      此函数返回一个空上下文。这通常只应在主请求处理程序或顶级请求处理程序中使用。这可用于为主函数、初始化、测试以及后续层或其他 goroutine 派生上下文的时候。

      ctx, cancel := context.Background()
      
        context.TODO() ctx Context

        此函数返回一个非 nil 的、空的上下文。没有任何值、不会被 cancel,不会超时,也没有截止日期。但是,这也应该仅在您不确定要使用什么上下文或者该函数还不能用于接收上下文时,可以使用这个方法,并且将在将来需要添加时使用。

        ctx, cancel := context.TODO()
        
          context.WithValue(parent Context, key, val interface{}) Context

          这个函数接受一个上下文并返回一个派生的上下文,其中值 val 与 key 相关联,并与上下文一起经过上下文树。

          WithValue 方法其实是创建了一个类型为 valueCtx 的上下文,它的类型定义如下:

          type valueCtx struct {
              Context
              key, val interface{}
          }
          

          这意味着一旦你得到一个带有值的上下文,任何从它派生的上下文都会得到这个值。该值是不可变的,因此是线程安全的。

          提供的键必须是可比较的,并且不应该是字符串类型或任何其他内置类型,以避免使用上下文的包之间发生冲突。 WithValue 的用户应该为键定义自己的类型。

          为避免在分配给 interface{} 时进行分配,上下文键通常具有具体类型 struct{}。或者,导出的上下文键变量的静态类型应该是指针或接口。

          package main
          import (
            "context"
            "fmt"
          )
          type contextKey string
          func main() {
            var authToken contextKey = "auth_token"
            ctx := context.WithValue(context.Background(), authToken, "Hello123456")
            fmt.Println(ctx.Value(authToken))
          }
          

          运行该代码:

          $ go run .           
          Hello123456

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

            此函数接收父上下文并返回派生上下文,返回 parent 的副本,只是副本中的 Done Channel 是新建的对象,它的类型是 cancelCtx。在这个派生上下文中,添加了一个新的 Done channel,该 channel 在调用 cancel 函数或父上下文的 Done 通道关闭时关闭。

            要记住的一件事是,我们永远不应该在不同的函数或层之间传递这个 cancel ,因为它可能会导致意想不到的结果。创建派生上下文的函数应该只调用取消函数。

            下面是一个使用 Done 通道演示 goroutine 泄漏的示例:

            package main
            import (
              "context"
              "fmt"
              "math/rand"
              "time"
            )
            func main() {
              rand.Seed(time.Now().UnixNano())
              ctx, cancel := context.WithCancel(context.Background())
              defer cancel()
              for char := range randomCharGenerator(ctx) {
                generatedChar := string(char)
                fmt.Printf("%v\n", generatedChar)
                if generatedChar == "o" {
                  break
                }
              }
            }
            func randomCharGenerator(ctx context.Context) <-chan int {
              char := make(chan int)
              seedChar := int('a')
              go func() {
                for {
                  select {
                  case <-ctx.Done():
                    fmt.Printf("found %v", seedChar)
                    return
                  case char <- seedChar:
                    seedChar = 'a' + rand.Intn(26)
                  }
                }
              }()
              return char
            }
            

            运行结果:

            $ go run .           
            a
            m
            q
            c
            l
            t
            o

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

              此函数从其父级返回派生上下文,返回一个 parent 的副本。

              当期限超过或调用取消函数时,该派生上下文将被取消。例如,您可以创建一个在未来某个时间自动取消的上下文,并将其传递给子函数。当该上下文由于截止日期用完而被取消时,所有获得该上下文的函数都会收到通知停止工作并返回。如果 parent 的截止日期已经早于 d,则上下文的 Done 通道已经关闭。

              下面是我们正在读取一个大文件的示例,该文件的截止时间为当前时间 2 毫秒。我们将获得 2 毫秒的输出,然后将关闭上下文并退出程序。

              package main
              import (
                  "bufio"
                  "context"
                  "fmt"
                  "log"
                  "os"
                  "time"
              )
              func main() {
                  // context with deadline after 2 millisecond
                  ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Millisecond))
                  defer cancel()
                  lineRead := make(chan string)
                  var fileName = "sample-file.txt"
                  file, err := os.Open(fileName)
                  if err != nil {
                      log.Fatalf("failed opening file: %s", err)
                  }
                  scanner := bufio.NewScanner(file)
                  scanner.Split(bufio.ScanLines)
                  // goroutine to read file line by line and passing to channel to print
                  go func() {
                      for scanner.Scan() {
                          lineRead <- scanner.Text()
                      }
                      close(lineRead)
                      file.Close()
                  }()
              outer:
                  for {
                      // printing file line by line until deadline is reached
                      select {
                      case <-ctx.Done():
                          fmt.Println("process stopped. reason: ", ctx.Err())
                          break outer
                      case line := <-lineRead:
                          fmt.Println(line)
                      }
                  }
              }
              
                func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

                这个函数类似于 context.WithDeadline。不同之处在于它将持续时间作为输入而不是时间对象。此函数返回一个派生上下文,如果调用取消函数或超过超时持续时间,该上下文将被取消。

                WithTimeout 的实现是:

                func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
                    // 当前时间+timeout就是deadline
                    return WithDeadline(parent, time.Now().Add(timeout))
                }
                

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

                package main
                import (
                    "bufio"
                    "context"
                    "fmt"
                    "log"
                    "os"
                    "time"
                )
                func main() {
                    // context with deadline after 2 millisecond
                    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
                    defer cancel()
                    lineRead := make(chan string)
                    var fileName = "sample-file.txt"
                    file, err := os.Open(fileName)
                    if err != nil {
                        log.Fatalf("failed opening file: %s", err)
                    }
                    scanner := bufio.NewScanner(file)
                    scanner.Split(bufio.ScanLines)
                    // goroutine to read file line by line and passing to channel to print
                    go func() {
                        for scanner.Scan() {
                            lineRead <- scanner.Text()
                        }
                        close(lineRead)
                        file.Close()
                    }()
                outer:
                    for {
                        // printing file line by line until deadline is reached
                        select {
                        case <-ctx.Done():
                            fmt.Println("process stopped. reason: ", ctx.Err())
                            break outer
                        case line := <-lineRead:
                            fmt.Println(line)
                        }
                    }
                }
                

                如果父上下文的 Done 通道关闭,它最终将关闭所有派生的 Done 通道(所有后代),如:

                package main
                import (
                    "context"
                    "fmt"
                    "time"
                )
                func main() {
                    c := make(chan string)
                    go func() {
                        time.Sleep(1 * time.Second)
                        c <- "one"
                    }()
                    ctx1 := context.Context(context.Background())
                    ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
                    ctx3, cancel3 := context.WithTimeout(ctx2, 10*time.Second) // derives from ctx2
                    ctx4, cancel4 := context.WithTimeout(ctx2, 3*time.Second)  // derives from ctx2
                    ctx5, cancel5 := context.WithTimeout(ctx4, 5*time.Second)  // derives from ctx4
                    cancel2()
                    defer cancel3()
                    defer cancel4()
                    defer cancel5()
                    select {
                    case <-ctx3.Done():
                        fmt.Println("ctx3 closed! error: ", ctx3.Err())
                    case <-ctx4.Done():
                        fmt.Println("ctx4 closed! error: ", ctx4.Err())
                    case <-ctx5.Done():
                        fmt.Println("ctx5 closed! error: ", ctx5.Err())
                    case msg := <-c:
                        fmt.Println("received", msg)
                    }
                }
                

                在这里,由于我们在创建其他派生上下文后立即关闭 ctx2,因此所有其他上下文也会立即关闭,随机打印 ctx3、ctx4 和 ctx5 关闭消息。 ctx5 是从 ctx4 派生的,由于 ctx2 关闭的级联效应,它正在关闭。尝试多次运行,您会看到不同的结果。

                使用 Background 或 TODO 方法创建的上下文没有取消、值或截止日期。

                package main
                import (
                    "context"
                    "fmt"
                )
                func main() {
                    ctx := context.Background()
                    _, ok := ctx.Deadline()
                    if !ok {
                        fmt.Println("no dealine is set")
                    }
                    done := ctx.Done()
                    if done == nil {
                        fmt.Println("channel is nil")
                    }
                }
                

                4.2>
                  不要将上下文存储在结构类型中;相反,将 Context 显式传递给需要它的每个函数。 Context 应该是第一个参数,通常命名为 ctx。
                  func DoSomething(ctx context.Context, arg Arg) error {
                      // ... use ctx ...
                  }
                  
                    不要传递 nil 上下文,即使函数允许。如果不确定要使用哪个 Context,请传递 context.TODO 或使用 context.Background() 创建一个空的上下文对象。仅使用上下文传递请求范围的数据。不要传递应该使用函数参数传递的数据。始终寻找 goroutine 泄漏并有效地使用上下文来避免这种情况。如果父上下文的 Done 通道关闭,它最终将关闭所有派生的 Done 通道(所有后代)上下文只是临时做函数之间的上下文传透,不能持久化上下文key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。

                    4.3>
                      上下文信息传递 (request-scoped),比如处理 http 请求、在请求处理链路上传递信息;控制子 goroutine 的运行;超时控制的方法调用;可以取消的方法调用。

                      5>

                      Context 是在 Go 中进行并发编程时最重要的工具之一。上下文的主要作用是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费。标准库中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。

                      参考链接:

                      Go Concurrency Patterns: Context

                      Simplifying context in Go

                      以上就是Go语言并发编程基础上下文概念详解的详细内容,更多关于Go语言并发上下文的资料请关注易采站长站其它相关文章!

                      如有侵权,请发邮件到 [email protected]

相关文章

  • 使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    (1)业务复杂度介绍 开门见山,假设一个直播间同时500W人在线,那么1秒钟1000条弹幕,那么弹幕系统的推送频率就是: 500W * 1000条/秒=50亿条/秒 ,想想B站2019跨年晚会那次弹幕系统得是
    2020-07-08
  • golang中import cycle not allowed解决的一种思路

    golang中import cycle not allowed解决的一种思路

    发现问题 项目中碰到了一些问题,使用了指针函数的思路来解决相应问题 在实际项目中,因为两个项目互相引了对方的一些方法,导致了循环引用的错误,原本可以使用http的请求来解
    2019-11-10
  • 从go语言中找&和*区别详解

    从go语言中找&和*区别详解

    *和的区别 : 是取地址符号 , 即取得某个变量的地址 , 如 ; a*是指针运算符 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值 . 从
    2020-06-23
  • Go语言中利用http发起Get和Post请求的方法示例

    Go语言中利用http发起Get和Post请求的方法示例

    关于 HTTP 协议 HTTP(即超文本传输协议)是现代网络中最常见和常用的协议之一,设计它的目的是保证客户机和服务器之间的通信。 HTTP 的工作方式是客户机与服务器之间的 “请求-应答
    2019-11-10
  • golang如何实现mapreduce单进程版本详解

    golang如何实现mapreduce单进程版本详解

    前言 MapReduce作为hadoop的编程框架,是工程师最常接触的部分,也是除去了网络环境和集群配 置之外对整个Job执行效率影响很大的部分,所以很有必要深入了解整个过程。元旦放假的第一天
    2019-11-10
  • Go打包二进制文件的实现

    Go打包二进制文件的实现

    背景 众所周知,go语言可打包成目标平台二进制文件是其一大优势,如此go项目在服务器不需要配置go环境和依赖就可跑起来。 操作 需求:打包部署到centos7 笔者打包环境:mac os 方法:
    2020-03-11
  • GO语言实现简单的目录复制功能

    GO语言实现简单的目录复制功能

    本文实例讲述了GO语言实现简单的目录复制功能。分享给大家供大家参考。具体实现方法如下: 创建一个独立的 goroutine 遍历文件,主进程负责写入数据。程序会复制空目录,也可以设
    2019-11-10
  • golang中定时器cpu使用率高的现象详析

    golang中定时器cpu使用率高的现象详析

    前言: 废话少说,上线一个用golang写的高频的任务派发系统,上线跑着很稳定,但有个缺点就是当没有任务的时候,cpu的消耗也在几个百分点。 平均值在3%左右的cpu使用率。你没有任务
    2019-11-10