Go语言并发编程基础上下文概念是什么 go编程题

   2023-02-09 学习力0
核心提示:本篇内容介绍了“Go语言并发编程基础上下文概念是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1 Go 中的 ContextGolang 的上下文也是应用开发

本篇内容介绍了“Go语言并发编程基础上下文概念是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

    1 Go 中的 Context

    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 接口

    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() 返回 nil

    • Value() : 在树状分布的 goroutine 之间共享数据,用 map 键值的工作方法,通过 key 值查询 value。

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

    context.TODO

    context.Background

    context.WithCancel

    context.WithValue

    context.WithTimeout

    context.WithDeadline

    3 Context Tree

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

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

    Go语言并发编程基础上下文概念是什么

    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 显式传递给需要它的每个函数。 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 Context 使用场景

    • 上下文信息传递 (request-scoped),比如处理 http 请求、在请求处理链路上传递信息;

    • 控制子 goroutine 的运行;

    • 超时控制的方法调用;

    • 可以取消的方法调用。

    “Go语言并发编程基础上下文概念是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注***网站,小编将为大家输出更多高质量的实用文章!

     
    标签: go语言
    反对 0举报 0 评论 0
     

    免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
        本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

    • Go语言使用goroutine及通道实现并发详解
      Go语言使用goroutine及通道实现并发详解
      目录使用通道接收数据阻塞接收数据非阻塞接收数据接收任意数据,忽略掉接收的数据循环接收数据使用通道接收数据在上一篇文章中介绍了通道以及使用通道发送数据,本篇接着了解通道的基本内容,如何使用通道接收数据;通道的接收同样使用"-"操作符;使用通道接
    • Go语言数据结构之希尔排序示例详解 go语言堆排序
      Go语言数据结构之希尔排序示例详解 go语言堆排
      目录希尔排序算法思想图解算法Go 代码实现:总结希尔排序在插入排序中,在待排序序列的记录个数比较少,而且基本有序,则排序的效率较高。1959 年,Donald Shell 从“减少记录个数” 和 “基本有序” 两个方面对直接插入排序进行了改进,提出了希尔排序
    • go语言 nil使用避坑指南
      目录引言nil默认值nil (重点记住)nil没有默认类型不同类型的nil值占用的内存大小可能是不一样的不同类型 nil 的指针是一样的不同类型的 nil 是不能比较的引言今天笔试题遇到 var x string = nil ,问这个定义是否正确?这里给出答案:cannot use nil as strin
      02-09 gonil避坑
    • Go语言开发保证并发安全实例详解 go语言处理高
      目录什么是并发安全?Mutex悲观锁乐观锁版本号机制CAS互斥锁读写互斥锁什么是并发安全?在高并发场景下,进程、线程(协程)可能会发生资源竞争,导致数据脏读、脏写、死锁等问题,为了避免此类问题的发生,就有了并发安全。这里举一个简单的例子: var data
    • Go语言包和包管理详解 go 常用包
      目录1 包简介1.1 工作空间1.2 源文件1.3 包命名1.4 main 包2导包2.1 两种方式2.2 包的别名2.3 简洁模式2.4非导入模式(匿名导入)2.5 导包的路径2.6 远程导入3 初始化 init3.1 init总结4 包管理4.1 演变过程4.2 Go Model优点4.3 启用go module4.4 GOPROXY5 go m
    • Go语言制作svg格式树形图的示例代码
      Go语言制作svg格式树形图的示例代码
      目录什么是SVGSVG定义SVG优点预定义元素圆形 circle直线 line文字 text结点SVG格式根结点子树结点叶结点结点坐标结点文本二叉树转SVG全部源代码最近一直在刷二叉树题目,但在要验证结果时,通常用中序遍历、层序遍历查看结果,验证起来没有画图来得直观,所有
      02-09 Go树形图
    • go语言int类型最大值 go int长度
      go语言int类型最大值 go int长度
        正数的补码是自己本身负数的补码是,先取反码(首尾不反),然后+1 2.  在Go语言中  ^0表示对0取反  我们假如是4位代表一个数字的话,最高位是符号位  0在计算机中用补码的形式存在是 : 0000  取反得到  :1111  (是-1在计算机中以补码
      02-09
    • go包管理工具glide使用方法 go语言包管理工具
      golang没有官方最佳管理方案,在go的世界里存在大量的自制解决方案。go语言的包是没有中间库统一管理的,通过使用go get命令从远程代码库(github.com,goolge code 等)拉取,直接跳过中间版本库的约束,让代码的拉取直接基于源代码版本控制库开发者间的协同直
      02-09
    • [Go语言]从Docker源码学习Go——指针和Structs
      这两天在看reflect这个包在Docker中的使用时,遇到了各种问题,最后虽然知道怎么用了。但是对于这块的原理还不是太懂,于是把"THE WAY TO GO"中关键的几章看了下。继续坚持往下写,争取能说明白。源码还是先看Docker中源码, docker/api/client/cli.gotype Doc
      02-09
    • thrift简单示例 (go语言)
      这个thrift的简单示例来自于官网 (http://thrift.apache.org/tutorial/go), 因为官方提供的例子简单易懂, 所以没有必要额外考虑新的例子. 关于安装的教程, 可以参考https://www.cnblogs.com/albizzia/p/10838646.html, 关于thrift文件的语法, 可以参考: https
      02-09
    点击排行