Go 并发编程基础:什么是上下文(下)

简介: 在开发过程中,也有这个上下文(Context)的概念,而且上下文也必不可少,缺少上下文,就不能获取完整的程序信息。那么什么是程序中的上下文呢?简单来说,就是在 API 之间或者函数调用之间,除了业务参数信息之外的额外信息。比如,服务器接收到客户端的 HTTP 请求之后,可以把客户端的 IP 地址和端口、客户端的身份信息、请求接收的时间、Trace ID 等信息放入到上下文中,这个上下文可以在后端的方法调用中传递。

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 的运行;
  • 超时控制的方法调用;
  • 可以取消的方法调用。

5 总结

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


相关文章
|
6天前
|
人工智能 Go 调度
掌握Go并发:Go语言并发编程深度解析
掌握Go并发:Go语言并发编程深度解析
|
28天前
|
安全 Go
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
|
3月前
|
并行计算 算法 安全
通过三个例子,学习 Go 语言并发编程的利器 - goroutine
通过三个例子,学习 Go 语言并发编程的利器 - goroutine
42 0
|
4月前
|
Go
通道多路复用:Go语言并发编程的黄金法则
通道多路复用:Go语言并发编程的黄金法则
36 0
|
4月前
|
安全 Go
Go新手步步为赢:并发编程通信指南
Go新手步步为赢:并发编程通信指南
25 0
|
5天前
|
程序员 Go
Golang深入浅出之-Select语句在Go并发编程中的应用
【4月更文挑战第23天】Go语言中的`select`语句是并发编程的关键,用于协调多个通道的读写。它会阻塞直到某个通道操作可行,执行对应的代码块。常见问题包括忘记初始化通道、死锁和忽视`default`分支。要解决这些问题,需确保通道初始化、避免死锁并添加`default`分支以处理无数据可用的情况。理解并妥善处理这些问题能帮助编写更高效、健壮的并发程序。结合使用`context.Context`和定时器等工具,可提升`select`的灵活性和可控性。
18 2
|
2月前
|
缓存 安全 Java
go并发编程
go的GMP并发模型,让go天然支持高并发,先了解一下GMP模型吧 GMP G协程,M工作线程、P处理器,M必须持有P才可以执行G P维护着一个协程G队列,P依次将G调度到M中运行 if M0中G0发生系统调用,M0将释放P,冗余的M1获取P,继续执行P队列中剩余的G。(只要P不空闲就充分利用了CPU) G0系统调用结束后,如果有空闲的P,则获取P继续执行G0,否则将G0放入全局队列,M0进入缓存池睡眠。(全局队列中的G主要来自从系统调用中恢复的G) 下面介绍一下编程常用的同步(synchronize)原语 互斥锁 mutex rwmutex,要了解自旋和饥饿模式 自旋最多4次,cpu核
32 1
|
1天前
|
Go
【Go语言专栏】Go语言的并发编程进阶:互斥锁与条件变量
【4月更文挑战第30天】本文探讨了Go语言中的互斥锁(Mutex)和条件变量(Condition Variable)在并发编程中的应用。互斥锁用于保护共享资源,防止多goroutine同时访问,通过Lock和Unlock进行控制,需注意避免死锁。条件变量则允许goroutine在条件满足时被唤醒,常与互斥锁结合使用以提高效率。了解和掌握这些同步原语能提升Go并发程序的性能和稳定性。进一步学习可参考Go官方文档和并发模式示例。
|
1天前
|
存储 Go
【Go 语言专栏】Go 语言的并发编程基础:goroutines 与 channels
【4月更文挑战第30天】Go 语言的并发编程基于goroutines和channels。Goroutines是轻量级线程,低成本并发执行。创建goroutine只需在函数调用前加`go`。Channels作为goroutines间通信和同步的桥梁,分无缓冲和有缓冲两种,可用`make`创建。结合使用goroutines和channels,可实现数据传递和同步,如通过无缓冲channels实现任务同步,或通过有缓冲channels传递数据。注意避免死锁、资源竞争,合理使用缓冲,以发挥Go并发优势。
|
5天前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
14 1