设为首页 加入收藏

TOP

golang标准库 context的使用(一)
2019-01-31 22:08:25 】 浏览:380
Tags:golang 标准 context 使用

本文索引

问题引入

goroutine为我们提供了轻量级的并发实现,作为golang最大的亮点之一更是备受推崇。

goroutine的简单固然有利于我们的开发,但简单总是有代价的,考虑如下例子:

func httpDo(req *http.Request, resp *http.Response) {
  for {
    select {
    case <-time.After(5 * time.Second):
      // 从req读取数据然后发送给resp

    // 其他的一些逻辑(如果有的话)
    }
  }
}

func startListener() {
  // start http listener
  for {
    req, resp := HTTPListener.Accept()
    go httpDo(req, resp)
  }
}

上面的例子中,goroutinehttpDo每隔5秒读取一次请求数据并发送给响应链接,startListener则每收到一个请求就启动一个goroutine去处理,虽然是伪代码,不过你已经发现了这是golang处理请求等并发任务时的惯用模型。

看着不是很简单吗,简单而又强大。确实如此,但有一个小问题。假如我的startListener崩溃了或者需要重新启动,这时前面那些链接都需要断开重连,那么我们应该怎么停止那些goroutine呢?

答案是做不到。原因很简单,当我们使用go func()启动一个goroutine后,除了channelsync包中的同步手段之外,我们没有任何可以控制goroutine的方法。简单的说,除非goroutine在函数体内return或者主goroutine终止运行,否则我们是不能通过外部手段干扰goroutine使其终止的。因此在上述例子中那些goroutine无法终止,这会造成goroutine leak。开头已经说过,goroutine足够轻量,通常对于一个函数体不是死循环的goroutine来说我们大可不必关心它的退出操作,然而对于例子中的goroutine来说它会持续运行下去,虽然每个goroutine只占用很少的资源,但如果数量足够大的话被浪费的资源是相当惊人的,而一个长时间运行的程序必然因为得不到释放的资源而出问题。更为致命的是这种leak的goroutine可能还会造成逻辑上的错误从而引发更严重的问题。

当然,一点简单的改造就可以避免问题,这也是goroutine的强大之处。前面我们提到channel等同步手段可以间接地控制goroutine,所以我们可以利用一个空chan来达到终止所有goroutine的目的:

func httpDo(req *http.Request, resp *http.Response, done <-chan struct{}) {
  for {
    select {
    case <-done:
      // 避免goroutine leak
      return
    case <-time.After(5 * time.Second):
      // 从req读取数据然后发送给resp

    // 其他的一些逻辑(如果有的话)
    }
  }
}

func startListener() {
  // start http listener

  done := make(chan struct{})
  defer close(done)
  for {
    req, resp := HTTPListener.Accept()
    go httpDo(req, resp, done)
  }
}

修改过的程序我们使用一个chan struct{}变量进行控制,当startListener退出时(无论正常结束还是panic)done都会关闭,关闭后的chan会返回对应类型0值,于是goroutine的select会收到done关闭的信号,随后跟着退出,goroutine leak被避免。

当然,这么做不够优雅,毕竟当startListener这样的函数增多后我们不得不每次都写大量重复的代码,这样会让开发变得乏味。

所以golang1.7引入了context包用来优雅地退出goroutine。

context包简介

golang为了实现优雅地退出goroutine,在1.7引入了context。虽然名字叫“上下文”(context)不过其实只是我们在上一节例子的包装。

context.Context是一个接口:

type Context interface {
    // 返回超时时间(duration加上创建context对象时的时间),如果已经超时ok为true
    // 返回的时间也可以是自己设置的time.Time
    Deadline() (deadline time.Time, ok bool)

    // done信号,和上一节的做法一样,这里进行了一些包装
    Done() <-chan struct{}

    // 如果Done未被关闭就返回nil。
    // 否则返回相应的错误,比如调用了cancel()会返回Canceled;超时会返回DeadlineExceeded
    Err() error

    // 可以给context设置一些值,使用方法和map类似,key需要支持==比较操作,value需要是并发安全的
    Value(key interface{}) interface{}
}

实现了Context接口的对象都是并发安全的(如果你自己实现了这个接口也必须确保并发安全)。

context的使用很简单,首先在需要产生goroutine的函数中创建一个context对象,然后将其作为goroutine的第一个参数传入,例如go func(ctx context.Context) {} (ctx),如果在goroutine里还会运行新的goroutine,那么就继续传递这个context对象。

如此一来最初的那个context对象就被称为parent, 其余goroutine中的被称为关联context,通过这种关系我们就可以把相关的goroutine联系在一起。

对于一个作为parent的context对象来说它也必须基于一个parent来创建,所以context提供了两个创建空context的函数:

func Background() Context
func TODO() Context

两者都返回一个空context,一个context不会被取消(cancel),也不会超时。它们唯一的区别是TODO表示你的代码正在准备使用context但仍然需要一些调整,这回告诉静态代码分析工具go vet不汇报某些context的使用错误,而通常我们应该使用Background产生的context来创建我们自己的context对象。

有了parent之后就可以创建我们需要的context对象了,context包提供了三种context,分别是是普通context,超时context以及带值的context:

// 普通context,通常这样调用ctx, cancel := context.WithCancel(context.Background())
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// 带超时的context,
首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇[Go] golang的竞争状态 下一篇[Go] go get获取官方库被墙解决

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目