菜鸟笔记
提升您的技术认知

深入理解Golang Context

阅读前需要掌握的内容

什么是pipeline( 流水线 )?

数据流水线充分利用了多核特性,代码层面是基于 channel 类型 和 go 关键字。

对于"流水线"这个概念,Go语言中并没有正式的定义,它只是很多种并发方式的一种。这里我给出一个非官方的定义:一条流水线是 是由多个阶段组成的,相邻的两个阶段由 channel 进行连接

每个阶段是由一组在同一个函数中启动的 goroutine 组成。在每个阶段,这些 goroutine 会执行下面三个操作:

  • 通过 inbound channels 从上游接收数据
  • 对接收到的数据执行一些操作,通常会生成新的数据
  • 将新生成的数据通过 outbound channels 发送给下游

除了第一个和最后一个阶段,每个阶段都可以有任意个 inbound 和 outbound channel。显然,第一个阶段只有 outbound channel,而最后一个阶段只有 inbound channel。我们通常称第一个阶段为"生产者"或"源头",称最后一个阶段为"消费者"或"接收者"。

流水线进阶:扇入和扇出

  • 扇出:同一个 channel 可以被多个函数读取数据,直到channel关闭。
    这种机制允许将工作负载分发到一组worker,以便更好地并行使用 CPU 和 I/O。

  • 扇入:多个 channel 的数据可以被同一个函数读取和处理,然后合并到一个 channel,直到所有 channel都关闭。

在使用流水线函数时,有一个固定的模式:

  • 在一个阶段,当所有发送操作 (ch<-) 结束以后,关闭 outbound channel

  • 在一个阶段,goroutine 会持续从 inbount channel 接收数据,直到所有 inbound channel 全部关闭

在这种模式下,每一个接收阶段都可以写成 range 循环的方式,
从而保证所有数据都被成功发送到下游后,goroutine能够立即退出。

但我们日常需求更多是:当后一个阶段不需要数据时,上游阶段能够停止生产

如果一个阶段不能消费所有的 inbound 数据,试图发送这些数据的 goroutine 会永久阻塞。显然这样会存在资源泄漏。一方面goroutine 消耗内存和运行时资源,另一方面goroutine 栈中的堆引用会阻止 gc 执行回收操作。

为了保证在下游阶段接收数据失败时,上游阶段也能够正常退出。一个方式是使用带有缓冲的管道作为 outbound channel。缓存可以存储固定个数的数据。如果缓存没有用完,那么发送操作会立即返回。但是缓存机制一班会存在一些问题,会使得代码变得相对脆弱,为了从根本上解决这个问题,我们需要提供一种机制,让下游阶段能够告知上游发送者停止接收的消息。

显式取消 (explicit cancellation)

当 main 函数决定退出,并停止接收 out 发送的任何数据时,它必须告诉上游阶段的 goroutine 让它们放弃
正在发送的数据。 main 函数通过发送数据到一个名为 done 的channel实现这样的机制,同时为了能够让未知数目、且个数不受限制的goroutine 停止向下游发送数据。在Go语言中,我们可以通过关闭一个channel 实现,因为在一个已关闭 channel 上执行接收操作(<-ch)总是能够立即返回,返回值是对应类型的零值。
因此。流水线通过两种方式解除发送者的阻塞:

  • 提供足够大的缓冲保存发送者发送的数据
  • 接收者放弃 channel 时,显式地通知发送者。

原文链接

context简介

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和rpc服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的Token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。 Context 包,就是专门用来简化对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作。

package context

context 包的核心是 struct Context,声明如下:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this `Context` is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this Context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
  • Done 方法返回一个 channel,这个 channel 对于以 Context 方式运行的函数而言,是一个取消信号。当这个 channel 关闭时,上面提到的这些函数应该终止手头的工作并立即返回。 之后,Err 方法会返回一个错误,告知为什么 Context 被取消。

  • 一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。背后的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。 一个典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。 作为一个折中,WithCancel 函数 (后面会细说) 提供了一种取消新的 Context 的方法。

  • Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,
    对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。

  • Deadline 方法允许函数确定它们是否应该开始工作。如果剩下的时间太少,也许这些函数就不值得启动。代码中,我们也可以使用 Deadline 对象为 I/O 操作设置截止时间。

  • Value 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。

继承 context

context 包提供了一些函数,协助用户从现有的 Context 对象创建新的 Context 对象。
这些 Context 对象形成一棵树:当一个 Context 对象被取消时,继承自它的所有 Context 都会被取消。

Background 是所有 Context 对象树的根,它不能被取消。它的声明如下:

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level `Context` for incoming requests.
func Background() Context

WithCancel 和 WithTimeout 函数 会返回继承的 Context 对象, 这些对象可以比它们的父 Context 更早地取消。

当请求处理函数返回时,与该请求关联的 Context 会被取消。 当使用多个副本发送请求时,可以使用 WithCancel取消多余的请求。 WithTimeout 在设置对后端服务器请求截止时间时非常有用。

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue 函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

结论

在 Google, 我们要求 Go 程序员把 Context 作为第一个参数传递给 入口请求和出口请求链路上的每一个函数。这种机制一方面保证了多个团队开发的 Go 项目能够良好地协作,另一方面它是一种简单的超时和取消机制,保证了临界区数据 (比如安全凭证) 在不同的 Go 项目中顺利传递。

如果你要在 Context 之上构建服务器框架,需要一个自己的 Context 实现,在框架与期望 Context 参数的代码之间建立一座桥梁。
当然,Client 库也需要接收一个 Context 对象。在请求作用域数据与取消之间建立了通用的接口以后,开发者使用 Context
分享代码、创建可扩展的服务都会非常方便。