Go语言进阶(五)Context

Channels

channels 是一种类型安全的消息队列,充当两个 goroutine 之间的管道,将通过它同步的进行任意资源的交换。chan 控制 goroutines 交互的能力从而创建了 Go 同步机制。当创建的 chan 没有容量时,称为无缓冲通道。反过来,使用容量创建的 chan 称为缓冲通道。

要了解通过 chan 交互的 goroutine 的同步行为是什么,我们需要知道通道的类型和状态。根据我们使用的是无缓冲通道还是缓冲通道,场景会有所不同,所以让我们单独讨论每个场景。

Unbuffered Channels

无缓冲 chan 没有容量,因此进行任何交换前需要两个 goroutine 同时准备好。当 goroutine 试图将一个资源发送到一个无缓冲的通道并且没有goroutine 等待接收该资源时,该通道将锁住发送 goroutine 并使其等待。当 goroutine 尝试从无缓冲通道接收,并且没有 goroutine 等待发送资源时,该通道将锁住接收 goroutine 并使其等待。

无缓冲信道的本质是保证同步。

  • Receive 先于 Send 发生。
  • 好处: 100% 保证能收到。
  • 代价: 延迟时间未知。

Buffered Channels

buffered channel 具有容量,因此其行为可能有点不同。当 goroutine 试图将资源发送到缓冲通道,而该通道已满时,该通道将锁住 goroutine并使其等待缓冲区可用。如果通道中有空间,发送可以立即进行,goroutine 可以继续。当goroutine 试图从缓冲通道接收数据,而缓冲通道为空时,该通道将锁住 goroutine 并使其等待资源被发送。

我们在 chan 创建过程中定义的缓冲区大小可能会极大地影响性能。我将使用密集使用 chan 的扇出模式来查看不同缓冲区大小的影响。在我们的基准测试中,一个 producer 将在通道中注入百万个整数元素,而5个 worker 将读取并将它们追加到一个名为 total 的结果变量中。

  • Send 先于 Receive 发生。
  • 好处: 延迟更小。
  • 代价: 不保证数据到达,越大的 buffer,越小的保障到达。buffer = 1 时,给你延迟一个消息的保障。

image-20211211203332784

image-20211211203337395

Go Concurrency Patterns(Go并行模式)

  • Timing out
  • Moving on
  • Pipeline
  • Fan-out, Fan-in
  • Cancellation
    • Close 先于 Receive 发生(类似 Buffered)。
    • 不需要传递数据,或者传递 nil。
    • 非常适合取消和超时控制。
  • Context

参考连接:

  • https://blog.golang.org/concurrency-timeouts
  • https://blog.golang.org/pipelines
  • https://talks.golang.org/2013/advconc.slide#1
  • https://github.com/go-kratos/kratos/tree/master/pkg/sync

Package Context

Request-scoped context(请求域上下文)

在 Go 服务中,每个传入的请求都在其自己的goroutine 中处理。请求处理程序通常启动额外的 goroutine 来访问其他后端,如数据库和 RPC 服务。处理请求的 goroutine 通常需要访问特定于请求(request-specific context)的值,例如最终用户的身份、授权令牌和请求的截止日期(deadline)。当一个请求被取消或超时时,处理该请求的所有 goroutine 都应该快速退出(fail fast),这样系统就可以回收它们正在使用的任何资源。

Go 1.7 引入一个 context 包,它使得跨 API 边界的请求范围元数据、取消信号和截止日期很容易传递给处理请求所涉及的所有 goroutine(显示传递)。

如何将 context 集成到 API 中?

在将 context 集成到 API 中时,要记住的最重要的一点是,它的作用域是请求级别的。例如,沿单个数据库查询存在是有意义的,但沿数据库对象存在则没有意义。

目前有两种方法可以将 context 对象集成到 API 中:

  • The first parameter of a function call(首参数传递 context 对象),如net包的Dialer.DialContext

  • Optional config on a request structure(在第一个 request 对象中携带一个可选的 context 对象。),如net/http 库的 Request.WithContext

Do not store Contexts inside a struct type(尽量不要在结构体中放Context)

使用 context 的一个很好的心智模型是它应该在程序中流动,应该贯穿你的代码。这通常意味着您不希望将其存储在结构体之中。它从一个函数传递到另一个函数,并根据需要进行扩展。理想情况下,每个请求都会创建一个 context 对象,并在请求结束时过期。

不存储上下文的一个例外是,当您需要将它放入一个结构中时,该结构纯粹用作通过通道传递的消息。如下例所示。

context.WithValue

context.WithValue 内部基于 valueCtx 实现:

为了实现不断的 WithValue,构建新的 context,内部在查找 key 时候,使用递归方式不断从当前,从父节点寻找匹配的 key,直到 root contextBackgrondTODO Value 函数会返回

image-20211211205628141

context.WithValue 方法允许上下文携带请求范围的数据。这些数据必须是安全的,以便多个 goroutine 同时使用。这里的数据,更多是面向请求的元数据,不应该作为函数的可选参数来使用(比如 context 里面挂了一个sql.Tx 对象,传递到 data 层使用),因为元数据相对函数参数更加是隐含的,面向请求的。而参数是更加显示的。

同一个 context 对象可以传递给在不同 goroutine 中运行的函数;上下文对于多个 goroutine 同时使用是安全的。对于值类型最容易犯错的地方,在于 context value 应该是 immutable 的,每次重新赋值应该是新的 context,即: context.WithValue(ctx, oldvalue)

https://pkg.go.dev/google.golang.org/grpc/metadata

Context.Value should inform, not control(Context.Value是用来挂载信息的,不是用来控制流程的)

当需要存储很多数据在Context.Value时,Context.Value链表式查询效率非常低,我们可以写一个基于map的上下文。

这样可以提高我们的查询效率,但是会带来新的问题,当两个 goroutine 同时使用作为函数签名传入,如果我们修改了 这个map,会导致另外进行读 context.Valuegoroutine 和修改 mapgoroutine,在 map 对象上产生 data race

image-20211211211320092

因此我们要使用 copy-on-write 的思路,解决跨多个 goroutine 使用数据、修改数据的场景。

Replace a Context using WithCancel, WithDeadline, WithTimeout, or WithValue.

image-20211211211408415

The chain of function calls between them must propagate the Context.(它们之间的函数调用链必须传播上下文。)

When a Context is canceled, all Contexts derived from it are also canceled

当一个 context 被取消时,从它派生的所有 context 也将被取消。WithCancel(ctx) 参数 ctx 认为是 parent ctx,在内部会进行一个传播关系链的关联。Done() 返回 一个 chan,当我们取消某个parent context, 实际上上会递归层层 cancel 掉自己的 child contextdone chan 从而让整个调用链中所有监听 cancelgoroutine 退出。

image-20211211212232579

如果要实现一个超时控制,通过上面的 contextparent/child 机制,其实我们只需要启动一个定时器,然后在超时的时候,直接将当前的 contextcancel 掉,就可以实现监听在当前和下层的额 context.Done()goroutine 的退出。