文章目录

  • 1.什么是 Context
  • 2.为什么要有 Context
  • 3.context 包源码一览
    • 3.1 Context
    • 3.2 CancelFunc
    • 3.3 canceler
    • 3.4 Context 的实现
      • 3.4.1 emptyCtx
      • 3.4.2 cancelCtx
      • 3.4.3 timerCtx
      • 3.4.4 valueCtx
  • 4.context 的用法
    • 4.1 使用建议
    • 4.2 传递共享的数据
    • 4.3 取消 goroutine
    • 4.4 防止 goroutine 泄漏
  • 5.Context 的不足
  • 6.小结
  • 参考文献

1.什么是 Context

Go 1.7 标准库引入 Context,中文名为上下文,是一个跨 API 和进程用来传递截止日期、取消信号和请求相关值的接口。

context.Context 定义如下:

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}

Deadline()返回一个完成工作的截止时间,表示上下文应该被取消的时间。如果 ok==false 表示没有设置截止时间。

Done()返回一个 Channel,这个 Channel 会在当前工作完成时被关闭,表示上下文应该被取消。如果无法取消此上下文,则 Done 可能返回 nil。多次调用 Done 方法会返回同一个 Channel。

Err()返回 Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空值。如果 Context 被取消,会返回context.Canceled 错误;如果 Context 超时,会返回context.DeadlineExceeded错误。

Value()从 Context 中获取键对应的值。如果未设置 key 对应的值则返回 nil。以相同 key 多次调用会返回相同的结果。

另外,context 包中提供了两个创建默认上下文的函数:

// TODO 返回一个非 nil 但空的上下文。
// 当不清楚要使用哪种上下文或无可用上下文尚时应该使用 context.TODO。
func TODO() Context// Background 返回一个非 nil 但空的上下文。
// 它不会被 cancel,没有值,也没有截止时间。它通常由 main 函数、初始化和测试使用,并作为处理请求的顶级上下文。
func Background() Context

还有四个基于父级创建不同类型上下文的函数:

// WithCancel 基于父级创建一个具有 Done channel 的 context
func WithCancel(parent Context) (Context, CancelFunc)// WithDeadline 基于父级创建一个不晚于 d 结束的 context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)// WithTimeout 等同于 WithDeadline(parent, time.Now().Add(timeout))
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)// WithValue 基于父级创建一个包含指定 key 和 value 的 context
func WithValue(parent Context, key, val interface{}) Context

在后面会详细介绍这些不同类型 context 的用法。

2.为什么要有 Context

Go 为后台服务而生,如只需几行代码,便可以搭建一个 HTTP 服务。

在 Go 的服务里,通常每来一个请求都会启动若干个 goroutine 同时工作:有些执行业务逻辑,有些去数据库拿数据,有些调用下游接口获取相关数据…
在这里插入图片描述
协程 a 生 b c d,c 生 e,e 生 f。父协程与子孙协程之间是关联在一起的,他们需要共享请求的相关信息,比如用户登录态,请求超时时间等。如何将这些协程联系在一起,context 应运而生。

话说回来,为什么要将这些协程关联在一起呢?以超时为例,当请求被取消或是处理时间太长,这有可能是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了这次请求结果。此时所有正在为这个请求工作的 goroutine 都需要快速退出,因为它们的“工作成果”不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关资源了。

总的来说 context 的作用是为了在一组 goroutine 间传递上下文信息(cancel signal,deadline,request-scoped value)以达到对它们的管理控制。

3.context 包源码一览

我们分析的 Go 版本依然是 1.17。

3.1 Context

context 是一个接口,某个类型只要实现了其申明的所有方法,便实现了 context。再次看下 context 的定义。

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}

方法的作用在前文已经详述,这里不再赘述。

3.2 CancelFunc

另外 context 包中还定义了一个函数类型 CancelFunc,

type CancelFunc func()

CancelFunc 通知操作放弃其工作。CancelFunc 不会等待工作停止。多个 goroutine 可以同时调用 CancelFunc。在第一次调用之后,对 CancelFunc 的后续调用不会执行任何操作。

3.3 canceler

context 包还定义了一个更加简单的用于取消操作的 context,名为 canceler,其定义如下。

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}
}

因其首字母小写,所以该接口未被导出,外部包无法直接使用,只在 context 包内使用。实现该接口的类型有 *cancelCtx*timerCtx

为什么其中一个方法 cancel() 首字母是小写,未被导出,而 Done() 确是导出一定要实现的呢?为何如此设计呢?

(1)“取消”操作应该是建议性,而非强制性。
caller 不应该去关心、干涉 callee 的情况,决定如何以及何时 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来做进一步的决策,因此接口并没有定义 cancel 方法。

(2)“取消”操作应该可传递。
“取消”某个函数时,和它相关联的其他函数也应该“取消”。因此,Done() 方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。

3.4 Context 的实现

context 包中定义了 Context 接口后,并且给出了四个实现,分别是:

  • emptyCtx
  • cancelCtx
  • timerCtx
  • valueCtx

我们可以根据不同场景选择使用不同的 Context。

3.4.1 emptyCtx

emptyCtx 正如其名,是一个空上下文。无法被取消,不携带值,也没有截止日期。

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx intfunc (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return
}func (*emptyCtx) Done() <-chan struct{} {return nil
}func (*emptyCtx) Err() error {return nil
}func (*emptyCtx) Value(key interface{}) interface{} {return nil
}

其未被导出,但被包装成如下两个变量,通过相应的导出函数对外提供使用。

var (background = new(emptyCtx)todo       = new(emptyCtx)
)// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {return background
}// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {return todo
}

从源代码来看,Background()TODO() 分别返回两个同类型不同的空上下文对象,没有太大的差别,只是在使用和语义上稍有不同:

  • Background() 是上下文的默认值,所有其他的上下文都应该从它衍生出来;比如用在 main 函数或作为最顶层的 context。
  • TODO() 通常用在并不知道传递什么 context 的情形下使用。如调用一个需要传递 context 参数的函数,你手头并没有现成 context 可以传递,这时就可以传递 todo。这常常发生在重构进行中,给一些函数添加了一个 Context 参数,但不知道要传什么,就用 todo “占个位子”,最终要换成其他 context。

3.4.2 cancelCtx

cancelCtx 是一个用于取消操作的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call
}

cancelCtx 是一个未导出类型,通过创建函数WithCancel()暴露给用户使用。

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {if parent == nil {panic("cannot create context from nil parent")}c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }
}// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}

传入一个父 Context(这通常是一个 background,作为根结点),返回新建的 Context,新 Context 的 done channel 是新建的。

注意: 从 cancelCtx 的定义和生成函数WithCancel()可以看出,我们基于父 Context 每生成一个 cancelCtx,相当于在一个树状结构的 Context 树中添加一个子结点。类似于下面这个样子:

先来看其Done()方法的实现:

func (c *cancelCtx) Done() <-chan struct{} {d := c.done.Load()if d != nil {return d.(chan struct{})}c.mu.Lock()defer c.mu.Unlock()d = c.done.Load()if d == nil {d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}

c.done 采用惰性初始化的方式创建,只有调用了Done()方法的时候才会被创建。再次说明,函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。

再看一下Err()String()方法,二者较为简单,Err() 用于返回错误信息,String()用于返回上下文名称。

func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}type stringer interface {String() string
}func contextName(c Context) string {if s, ok := c.(stringer); ok {return s.String()}return reflectlite.TypeOf(c).String()
}func (c *cancelCtx) String() string {return contextName(c.Context) + ".WithCancel"
}

下面重点看下cancel()方法的实现。

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {if err == nil {panic("context: internal error: missing cancel error")}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errd, _ := c.done.Load().(chan struct{})if d == nil {c.done.Store(closedchan)} else {close(d)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}

从方法描述来看,cancel()方法的功能就是关闭 channel(c.done)来传递取消信息,并且递归地取消它的所有子结点;如果入参 removeFromParent 为 true,则从父结点从删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子结点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中。

当 WithCancel() 函数返回的 CancelFunc 被调用或者父结点的 done channel 被关闭(父结点的 CancelFunc 被调用),此 context(子结点) 的 done channel 也会被关闭。

注意传给cancel()方法的参数,前者是 true,也就是说取消的时候,需要将自己从父结点里删除。第二个参数则是一个固定的取消错误类型:

var Canceled = errors.New("context canceled")

还要注意到一点,调用子结点 cancel 方法的时候,传入的第一个参数 removeFromParent 是 false。

removeFromParent 什么时候会传 true,什么时候传 false 呢?

先看一下当 removeFromParent 为 true 时,会将当前 context 从父结点中删除操作。

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()
}

其中delete(p.children, child)就是完成从父结点 map 中删除自己。

什么时候会传 true 呢?答案是调用 WithCancel() 方法的时候,也就是新创建一个用于取消的 context 结点时,返回的 cancelFunc 函数会传入 true。这样做的结果是:当调用返回的 cancelFunc 时,会将这个 context 从它的父结点里“除名”,因为父结点可能有很多子结点,我自己取消了,需要清理自己,从父亲结点删除自己。

在自己的cancel()方法中,我所有的子结点都会因为c.children = nil完成断绝操作,自然就没有必要在所有的子结点的cancel() 方法中一一和我断绝关系,没必要一个个做。

在这里插入图片描述
如上左图,代表一棵 Context 树。当调用左图中标红 Context 的 cancel 方法后,该 Context 从它的父 Context 中去除掉了:实线箭头变成了虚线。且虚线圈框出来的 Context 都被取消了,圈内的 context 间的父子关系都荡然无存了。

在生成 cancelCtx 的函数WithCancel()有一个操作需要注意一下,便是propagateCancel(parent, &c)

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {done := parent.Done()if done == nil {return // parent is never canceled}select {case <-done:// parent is already canceledchild.cancel(false, parent.Err())returndefault:}if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else {atomic.AddInt32(&goroutines, +1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}

该函数的作用就是将生成的当前 cancelCtx 挂靠到“可取消”的父 Context,这样便形成了上面描述的 Context 树,当父 Context 被取消时,能够将取消操作传递至子 Context。

这里着重解释下为什么会有 else 描述的情况发生。else 是指当前结点 Context 没有向上找到可以取消的父结点,那么就要再启动一个协程监控父结点或者子结点的取消动作。

这里就有疑问了,既然没找到可以取消的父结点,那case <-parent.Done()这个 case 就永远不会发生,所以可以忽略这个 case;而case <-child.Done()这个 case 又啥事不干。那这个 else 不就多余了吗?

其实不然,我们来看parentCancelCtx()的代码:

// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
func parentCancelCtx(parent Context) (*cancelCtx, bool) {done := parent.Done()if done == closedchan || done == nil {return nil, false}p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {return nil, false}pdone, _ := p.done.Load().(chan struct{})if pdone != done {return nil, false}return p, true
}

如果 parent 携带的 value 并不是一个 *cancelCtx,那么就会判断为不可取消。这种情况一般发生在一个 struct 匿名嵌套了 Context,就识别不出来了,因为parent.Value(&cancelCtxKey)返回的是*struct,而不是*cancelCtx

3.4.3 timerCtx

timerCtx 是一个可以被取消的计时器上下文,基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 Context。

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}

timerCtx 首先是一个 cancelCtx,所以它能取消。看下其cancel()方法:

func (c *timerCtx) cancel(removeFromParent bool, err error) {c.cancelCtx.cancel(false, err)if removeFromParent {// Remove this timerCtx from its parent cancelCtx's children.removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {c.timer.Stop()c.timer = nil}c.mu.Unlock()
}

同样地,timerCtx 也是一个未导出类型,其对应的创建函数是WithTimeout()

// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// 	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// 		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// 		defer cancel()  // releases resources if slowOperation completes before timeout elapses
// 		return slowOperation(ctx)
// 	}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}

该函数直接调用了WithDeadline(),传入的 deadline 是当前时间加上 timeout 的时间,也就是从现在开始再经过 timeout 时间就算超时。也就是说,WithDeadline()需要用的是绝对时间。重点来看下:

// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {if parent == nil {panic("cannot create context from nil parent")}if cur, ok := parent.Deadline(); ok && cur.Before(d) {// The current deadline is already sooner than the new one.return WithCancel(parent)}c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}propagateCancel(parent, c)dur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}

也就是说仍然要把子节点挂靠到父结点,一旦父结点取消了,会把取消信号向下传递到子结点,子结点随之取消。

有一个特殊情况是,如果要创建的这个子结点的 deadline 比父结点要晚,也就是说如果父结点是时间到自动取消,那么一定会取消这个子结点,导致子结点的 deadline 根本不起作用,因为子结点在 deadline 到来之前就已经被父结点取消了。

这个函数最核心的一句是:

c.timer = time.AfterFunc(d, func() {c.cancel(true, DeadlineExceeded)
})

c.timer 会在 d 时间间隔后,自动调用 cancel 函数,并且传入的错误就是超时错误DeadlineExceeded

// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{}type deadlineExceededError struct{}func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

3.4.4 valueCtx

valueCtx 是一个只用于传值的 Context,其携带一个键值对,其他的功能则委托给内嵌的 Context。

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {Contextkey, val interface{}
}

看下其实现的两个方法。

func (c *valueCtx) String() string {return contextName(c.Context) + ".WithValue(type " +reflectlite.TypeOf(c.key).String() +", val " + stringify(c.val) + ")"
}func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}

由于它直接将 Context 作为匿名字段,因此仅管它只实现了 2 个方法,其他方法继承自父 Context。但它仍然是一个 Context,这是 Go 语言的一个特点。

同样地,valueCtx 也是一个未导出类型,其对应的创建函数是WithValue()

// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {if parent == nil {panic("cannot create context from nil parent")}if key == nil {panic("nil key")}if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}

对 key 的要求是可比较,因为之后需要通过 key 取出 context 中的值,可比较是必须的。

通过层层传递 context,最终形成这样一棵树:

和链表有点像,只是它的方向相反。Context 指向它的父结点,链表则指向下一个结点。通过WithValue()函数,可以创建层层的 valueCtx,存储 goroutine 间可以共享的变量。

取值的过程,实际上是一个递归查找的过程。再次看一下其Value()方法。

func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}

因为查找方向是往上走的,所以,父结点没法获取子结点存储的值,子结点却可以获取父结点的值。

WithValue 创建 Context 结点的过程实际上就是创建链表节点的过程。两个结点的 key 值是可以相等的,但它们是两个不同的 Context 结点。查找的时候,会向上查找到最后一个挂载的 Context 结点,也就是离得比较近的一个父结点 Context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表。

如果你接手过项目,肯定经历过这样的窘境:在一个处理过程中,有若干子函数、子协程。各种不同的地方会向 context 里塞入各种不同的 k-v 对,最后在某个地方使用。

你根本就不知道什么时候什么地方传了什么值?这些值会不会被“覆盖”(底层是两个不同的 Context 节点,查找的时候,只会返回一个结果)?你肯定会崩溃的。

而这也是 Context 最受争议的地方,很多人建议尽量不要通过 Context 传值。

4.context 的用法

4.1 使用建议

一般情况下,我们使用Background()获取一个空的 Context 作为根节点,有了根结点 Context,便可以根据不同的业务场景选择使用如下四个函数创建对应类型的子 Context。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

官方 context 包说明文档中已经给出了 context 的使用建议:

1.Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.

2.Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.

3.Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

4.The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

对应的中文释义为:
1.不要将 Context 塞到结构体里;直接将 Context 类型作为函数的第一参数,且命名为 ctx。

2.不要向函数传入一个 nil Context,如果你实在不知道传哪个 Context 请传 context.TODO。

3.不要把本应该作为函数参数的数据放到 Context 中传给函数,Context 只存储请求范围内在不同进程和 API 间共享的数据(如登录信息 Cookie)。

4.同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。

4.2 传递共享的数据

对于 Web 服务端开发,往往希望将一个请求处理的整个过程串起来,这就非常依赖于 Thread Local(对于 Go 可理解为单个协程所独有) 的变量,而在 Go 语言中并没有这个概念,因此需要在函数调用的时候传递 Context。

package mainimport ("context""fmt"
)func main() {ctx := context.Background()process(ctx)ctx = context.WithValue(ctx, "traceID", "foo")process(ctx)
}func process(ctx context.Context) {traceId, ok := ctx.Value("traceID").(string)if ok {fmt.Printf("process over. trace_id=%s\n", traceId)} else {fmt.Printf("process over. no trace_id\n")}
}

运行输出:

process over. no trace_id
process over. trace_id=foo

当然,现实场景中可能是从一个 HTTP 请求中获取到 Request-ID。所以,下面这个样例可能更适合:

const requestIDKey int = 0func WithRequestID(next http.Handler) http.Handler {return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {// 从 header 中提取 request-idreqID := req.Header.Get("X-Request-ID")// 创建 valueCtx。使用自定义的类型,不容易冲突ctx := context.WithValue(req.Context(), requestIDKey, reqID)// 创建新的请求req = req.WithContext(ctx)// 调用 HTTP 处理函数next.ServeHTTP(rw, req)})
}// 获取 request-id
func GetRequestID(ctx context.Context) string {ctx.Value(requestIDKey).(string)
}func Handle(rw http.ResponseWriter, req *http.Request) {// 拿到 reqId,后面可以记录日志等等reqID := GetRequestID(req.Context())...
}func main() {handler := WithRequestID(http.HandlerFunc(Handle))http.ListenAndServe("/", handler)
}

4.3 取消 goroutine

Context 的作用是为了在一组 goroutine 间传递上下文信息,其重便包括取消信号。取消信号可用于通知相关的 goroutine 终止执行,避免无效操作。

我们先来设想一个场景:打开外卖的订单页,地图上显示外卖小哥的位置,而且是每秒更新 1 次。app 端向后台发起 websocket 连接(现实中可能是轮询)请求后,后台启动一个协程,每隔 1 秒计算 1 次小哥的位置,并发送给端。如果用户退出此页面,则后台需要“取消”此过程,退出 goroutine,系统回收资源。

后端可能的实现如下:

func Perform() {for {calculatePos()sendResult()time.Sleep(time.Second)}
}

如果需要实现“取消”功能,并且在不了解 Context 功能的前提下,可能会这样做:给函数增加一个指针型的 bool 变量,在 for 语句的开始处判断 bool 变量是发由 true 变为 false,如果改变,则退出循环。

上面给出的简单做法,可以实现想要的效果。没有问题,但是并不优雅。并且一旦通知的信息多了之后,函数入参就会很臃肿复杂。优雅的做法,自然就要用到 Context。

func Perform(ctx context.Context) {for {calculatePos()sendResult()select {case <-ctx.Done():// 被取消,直接返回returncase <-time.After(time.Second):// block 1 秒钟 }}
}

主流程可能是这样的:

ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)// ……
// app 端返回页面,调用cancel 函数
cancel()

注意一个细节,WithTimeOut 函数返回的 Context 和 cancelFun 是分开的。Context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子结点 Context 调用取消函数,从而严格控制信息的流向:由父结点 Context 流向子结点 Context。

4.4 防止 goroutine 泄漏

前面那个例子里,goroutine 还是会自己执行完,最后返回,只不过会多浪费一些系统资源。这里给出一个如果不用 context 取消,goroutine 就会泄漏的例子(源自Using contexts to avoid leaking goroutines)。

// gen 是一个整数生成器且会泄漏 goroutine
func gen() <-chan int {ch := make(chan int)go func() {var n intfor {ch <- nn++time.Sleep(time.Second)}}()return ch
}

上面的生成器会启动一个具有无限循环的 goroutine,调用者会从信道这些值,直到 n 等于 5。

for n := range gen() {fmt.Println(n)if n == 5 {break}
}

当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会无限循环,永远不会停下来。发生了 goroutine 泄漏。

我们可以使用 Context 主动通知 gen 函数的协程停止执行,阻止泄漏。

func gen(ctx context.Context) <-chan int {ch := make(chan int)go func() {var n intfor {select {case <-ctx.Done():return // 当 ctx 结束时避免 goroutine 泄漏case ch <- n:n++}}}()return ch
}

现在,调用方可以在完成后向生成器发送信号。调用 cancel 函数后,内部 goroutine 将返回。

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // make sure all paths cancel the context to avoid context leakfor n := range gen(ctx) {fmt.Println(n)if n == 5 {cancel()break}
}// ...

5.Context 的不足

Context 的作用很明显,当我们在开发后台服务时,能帮助我们完成对一组相关 goroutine 的控制并传递共享数据。注意是后台服务,而不是所有的场景都需要使用 Context。

Go 官方建议我们把 Context 作为函数的第一个参数,甚至连名字都准备好了。这造成一个后果:因为我们想控制所有的协程的取消动作,所以需要在几乎所有的函数里加上一个 Context 参数。很快,我们的代码里,context 将像病毒一样扩散的到处都是。

另外,像 WithCancel、WithDeadline、WithTimeout、WithValue 这些创建函数,实际上是创建了一个个的链表结点而已。我们知道,对链表的操作,通常都是 O(n) 复杂度的,效率不高。

Context 解决的核心问题是 cancelation,即便它不完美,但它却简洁地解决了这个问题。

6.小结

到这里,整个 context 包的内容就全部讲完了。源码精炼简单,很适合学习阅读。

使用上,先创建一个根结点的 Context,之后根据 context 包提供的四个函数创建相应功能的子结点 context。由于它是并发安全的,所以可以放心地传递。

Go 1.7 引入 context 包,目的是为了解决一组相关 goroutine 的取消问题,当然还可以用于传递一些共享的数据。这种场景往往在开发后台 server 时遇到,所以 context 有其适用的场景,而非所有场景。

context 并不完美,有固定的使用场景,切勿滥用。


参考文献

pkg.go.dev/context
The Go Blog.Go Concurrency Patterns: Context
Using contexts to avoid leaking goroutines
Context should go away for Go 2
Go 语言设计与实现.上下文 Context
知乎.深度解密Go语言之context

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 带你读书之“红宝书”:第三章 语法基础(中)之 3.6. 流控制语句①

    「这是我参与2022首次更文挑战的第17天&#xff0c;活动详情查看&#xff1a;2022首次更文挑战」 写在前头 大多数小伙伴看技术书籍都会用“啃”来描述读书的直观感受&#xff0c;当然我也是一个前端小白&#xff0c;白的透明那种&#xff0c;但是我在读技术书籍感觉到“啃”…...

    2024/4/16 21:44:18
  2. 新版WIN10 [20H2(2009)及以上] 换回旧版电脑系统属性界面的几种方法

    方法一&#xff1a;彻底回复旧的系统信息界面 1、打开注册表&#xff0c;然后找到以下两个路径&#xff1a; HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\FeatureManagement\Overrides\0\2093230218 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FeatureManag…...

    2024/4/13 17:34:50
  3. React Route: 使用 useHistory 实现编程式导航

    const history useHistory();useHistory() 返回一个对象&#xff0c;可以使用两种方法&#xff1a;push 和 replace 实现编程式导航&#xff0c;push 增加新页&#xff0c;可以使用后退按钮返回到旧的页面&#xff0c;使用 replace 则不能返回。 sample code&#xff1a; im…...

    2024/4/17 12:58:37
  4. 设计模式的七大原则(一)

    设计原则一、前言二、单一职责原则2.1、概念2.2、代码演示2.2、单一职责原则注意事项和细节三、接口隔离原则3.1、基本介绍3.2、传统方法的问题和使用接口隔离原则改进3.3、代码演示一、前言 通常很多人都认为设计模式原则是六个&#xff0c;而我这里写了七个。大家不要以为是…...

    2024/4/17 22:00:43
  5. Windows安装OpenCV——利用MinGW+CMake从源码编译

    从源码编译(使用MinGW) 从源码编译一般是为了定制一些安装选项&#xff0c;比如想要获取samples、希望交叉编译安装cuda、需要安装contrib模块。此教程是通过MinGWCMake完成opencv的编译。因此你需要安装CMake&#xff1a;Download | CMake 和MinGW&#xff08;MinGW建议通过M…...

    2024/4/13 17:34:40
  6. 知识补充3:Python——类的继承

    类的基础 class Dog:# 从空白中创建这个类&#xff0c;所以定义中没有圆括号# 类的首字母要大写def __init__(self, name, age):# init两边各两个下划线&#xff0c;这是一种特殊方法# 这个方法定义成包含三个形参&#xff08;首位必须有且为self&#xff09;# 根据Dog类创建新…...

    2024/4/5 3:09:26
  7. 神经网络 对抗网络

    链接: https://pan.baidu.com/s/15mmxEks5SLOnn7QC0d7_xg?pwds2ca 提取码: s2ca...

    2024/4/13 6:51:16
  8. 树状数组入门

    树状数组是一种存储方式&#xff0c;比较方便的读写 //区间查询 int a[maxn], c[maxn]; int lowbit(int x) {return x & (-x); } int getsum(int x) {int sum 0;for (; x; x - lowbit(x))sum c[x];return sum; } void add(int x) {for (; x < maxn; x lowbit(x))c[x…...

    2024/4/13 17:35:30
  9. Scrapy shell的使用【python爬虫入门进阶】(22)

    您好&#xff0c;我是码农飞哥&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f601; 1. 社区逛一逛&#xff0c;周周有福利&#xff0c;周周有惊喜。码农飞哥社区&#xff0c;飞跃计划 &#x1f4aa;&#x1f3fb; 2. Python基础专栏&#xff0c;基础知识一网…...

    2024/4/13 17:35:35
  10. 【LeetCode】2. 两数相加(笨比办法)

    题目&#xff1a; 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这…...

    2024/4/17 21:48:39
  11. 定时备份window10下的文件

    前几天搭建了一个博客&#xff0c;然后想做一个本地备份&#xff0c;于是就有了以下的代码。博客文件夹路径为D:\Blog&#xff0c;要备份到的路径为D:Blog_back_up&#xff0c;命名的格式为年-月-日-blog。大家可以根据自己的需求修改下面的脚本。 详细步骤 创建脚本 创建一…...

    2024/4/15 4:31:13
  12. 求完数

    一个数如果恰好等于它的因子之和&#xff0c;这个数称为“完数”。例如6的因子分别为 1、2、3&#xff0c;而6123&#xff0c;因此6是“完数”。编程序找出制定正整数之内所有完数并输出。 编程提示&#xff1a;根据完数的定义可知&#xff0c;将一个数因式分解&#xff0c;所有…...

    2024/4/7 20:22:47
  13. JavaScript面试题集(三)

    function foo1() { return { bar: “hello” }; } function foo2() { return { bar: “hello” }; } 2&#xff1a; if (!(“a” in window)) { var a 1; } alert(a); 3&#xff1a; var a 1, b function a(x) { x && a(–x); }; alert(a); 4&#xff1a; function…...

    2024/4/19 9:05:11
  14. 大数据项目之Flink实时数仓(DWM层)

    设计思路 之前通过分流等处理手段&#xff0c;将数据拆分成了独立的kafka topic&#xff0c;接下来处理数据&#xff0c;我们应该考虑的是将实时计算使用的指标项进行处理&#xff0c;时效性是实时数仓所追求的&#xff0c;所以在一些场景没有必要和离线数仓一样&#xff0c;大…...

    2024/4/13 8:11:10
  15. 关于第三方图片资源403问题

    为什么文章列表数据中的好多图片资源请求失败返回 403&#xff1f; 这是因为我们项目的接口数据是后端通过爬虫抓取的第三方平台内容&#xff0c;而第三方平台对图片资源做了防盗链保护处理。第三方平台怎么处理图片资源保护的&#xff1f; 服务端一般使用 Referer 请求头识别…...

    2024/4/7 20:22:42
  16. 最大公约数和最小公倍数

    输入两个正整数m和n&#xff0c;求其最大公约数和最小公倍数。 编程提示:求两个正整数的最大公约数和最小公倍数采用的是欧几里得算法&#xff0c;也就是我们常说的辗转相除法。该算法如下&#xff1a; (1) 对于已知两数m&#xff0c;n&#xff0c;使得m>n (2) m除以n得余数…...

    2024/4/13 17:35:50
  17. MySQL 事务的四大特征

    1、原子性&#xff1a;不可分割的最小操作单位&#xff0c;要么同时成功&#xff0c;要么同时失败。 2、持久性&#xff1a;当事物提交或回滚后&#xff0c;数据库会持久化的数据保存。 3、隔离性&#xff1a;多个事务之间相互独立。 4、一致性&#xff1a;事务操作前后&#x…...

    2024/4/7 20:22:40
  18. RK3399平台开发系列讲解(内存篇)15.17、物理内存的组织形式

    目录 一、两种内存访问架构 UMA架构 NUMA架构 二、物理内存布局 Linux内核将物理内存简单的分为两部分: 用户物理内存空间和内核物理内存空间。当用户进程需要访问物理内存时&#xff0c;通过虚拟地址映射到用户物理内存空间这篇博客简单的介绍物理内存组织的相关概念和数据…...

    2024/4/18 4:28:03
  19. 神经网络模型代码

    # 神级网络 训练模型 import numpyclass neuralNetwork:def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):self.inodes inputnodes # x_train_data 数据长度self.hnodes hiddennodes # 节点个数self.onodes outputnodes # 输出节点个数self.…...

    2024/4/13 17:35:45
  20. JavaScript面试题集(二)

    1.附加题 模板引擎实践 HTML <!doctype html public ahh hell yeah> <html> <head><meta charsetutf-8><title>Simple Templating</title> </head> <body><div class"result"></div><script type&q…...

    2024/4/13 17:35:40

最新文章

  1. 宾馆酒店常用的实名认证接口有哪些?

    宾馆酒店在提供住宿服务的同时&#xff0c;确保客人身份的真实性和安全性是至关重要的&#xff0c;实名认证技术的应用&#xff0c;不仅有助于防止非法活动&#xff0c;还能提升客户的信任度和满意度。以下是宾馆酒店最常用的实名认证方式介绍&#xff1a; 身份证OCR识别登记 …...

    2024/4/19 16:57:51
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. Go语言map、slice、channel底层实现(go面试)

    slice 切片是一个引用类型&#xff0c;其底层实现是一个结构体&#xff0c;包含以下字段&#xff1a; ptr&#xff1a;一个指向底层数组的指针&#xff0c;指针指向数组的第一个元素。 len&#xff1a;切片当前包含的元素数量。 cap&#xff1a;切片的容量&#xff0c;即底层…...

    2024/4/19 7:50:33
  4. #QT项目实战(天气预报)

    1.IDE&#xff1a;QTCreator 2.实验&#xff1a; 3.记录&#xff1a; &#xff08;1&#xff09;调用API的Url a.调用API获取IP whois.pconline.com.cn/ipJson.jsp?iphttp://whois.pconline.com.cn/ipJson.jsp?ip if(window.IPCallBack) {IPCallBack({"ip":&quo…...

    2024/4/18 20:00:55
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/19 14:24:02
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/19 10:27:58
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/19 11:57:31
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/19 11:57:31
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/19 11:57:52
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/19 11:57:53
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/19 11:58:14
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/19 11:58:20
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/19 11:58:32
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/19 11:58:39
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/19 11:58:51
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/19 11:59:02
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/19 11:59:15
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/19 11:59:23
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/19 11:59:44
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/19 11:59:48
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/19 12:00:06
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/19 16:57:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/19 12:00:25
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/19 12:00:40
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57