Golang 中 context 的使用方式(一)

简介: Golang 中 context 的使用方式(一)

context 是 go 中控制协程的一种比较方便的方式。


Select + Chan


我们都知道一个 goroutine 启动后,我们是无法控制他的,大部分情况是等待它自己结束,那么如果这个 goroutine 是一个不会自己结束的后台 goroutine 呢?比如监控等,会一直运行的。

这种情况下比较笨的办法是全局变量,其他地方通过修改这个变量完成结束通知,然后后台 goroutine 不停的检查这个变量,如果发现被通知关闭了,就自我结束。这种方式首先我们要保证这个变量在多线程下的安全,基于此,有一种经典的处理方式:chan + select


func main() {
  stop := make(chan bool)
  go func() {
    for {
      select {
      case <-stop:    // 收到了停滞信号
        fmt.Println("监控退出,停止了...")
        return
      default:
        fmt.Println("goroutine监控中...")
        time.Sleep(2 * time.Second)
      }
    }
  }()
  time.Sleep(10 * time.Second)
  fmt.Println("可以了,通知监控停止")
  stop<- true
  //为了检测监控过是否停止,如果没有监控输出,就表示停止了
  time.Sleep(5 * time.Second)
}


这里还有一个额外的知识,go 中通道的接收是有阻塞和非阻塞的(发送只有阻塞),这里 select 中的 case 语句接收通道数据其实是非阻塞的。非阻塞接收通道数据的 CPU 消耗较高,但是可以获取是否通道中有数据的状态。但是当 case 上读一个通道时,如果这个通道是 nil,则该 case 永远阻塞


例子中我们定义一个 stop 的 chan,通知他结束后台 goroutine。实现也非常简单,在后台 goroutine 中,使用 select 判断 stop 是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行 default 里的监控逻辑,继续监控,只到收到 stop 的通知。

有了以上的逻辑,我们就可以在其他 goroutine 中,给 stop chan 发送值了,例子中是在 main goroutine 中发送的,控制让这个监控的 goroutine 结束。

发送了 stop<-true 结束的指令后,我这里使用 time.Sleep(5 * time.Second) 故意停顿5秒来检测我们结束监控 goroutine 是否成功。如果成功的话,不会再有 goroutine监控中... 的输出了;如果没有成功,监控 goroutine 就会继续打印 goroutine监控中... 输出。

这种 chan+select 的方式,是比较优雅的结束一个 goroutine 的方式,不过这种方式也有局限性,如果有很多 goroutine 都需要控制结束怎么办呢?如果这些 goroutine 又衍生了其他更多的 goroutine 怎么办呢?如果一层层的无穷尽的 goroutine 呢?这就非常复杂了,即使我们定义很多 chan 也很难解决这个问题,因为 goroutine 的关系链就导致了这种场景非常复杂。


Context


上面说的这种场景是存在的,比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine。所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是 Go 语言为我们提供的 Context,称之为上下文非常贴切,它就是 goroutine 的上下文。

下面我们就使用 Go Context 重写上面的示例。


func main() {
  ctx, cancel := context.WithCancel(context.Background())
  go func(ctx context.Context) {
    for {
      select {
      case <-ctx.Done():
        fmt.Println("监控退出,停止了...")
        return
      default:
        fmt.Println("goroutine监控中...")
        time.Sleep(2 * time.Second)
      }
    }
  }(ctx)
  time.Sleep(10 * time.Second)
  fmt.Println("可以了,通知监控停止")
  cancel()
  //为了检测监控过是否停止,如果没有监控输出,就表示停止了
  time.Sleep(5 * time.Second)
}


重写也很简单,把之前使用 select+chan 控制的协程改为 Context 控制即可:

context.Background() 返回一个空的 Context,这个空的 Context 一般用于整个 Context 树的根节点。然后我们使用 context.WithCancel(parent) 函数,创建一个可取消的子 Context,然后当作参数传给 goroutine 使用,这样就可以使用这个子 Context 跟踪这个 goroutine。

在 goroutine 中,使用 select 调用 <-ctx.Done() 判断是否要结束,如果接受到值的话,就可以返回结束 goroutine 了;如果接收不到,就会继续进行监控。

那么 Context 是如何发送结束指令的呢?这就是示例中的 cancel 函数啦,它是我们调用 context.WithCancel(parent) 函数生成子 Context 的时候返回的,第二个返回值就是这个取消函数,它是 CancelFunc 类型的。我们调用它就可以发出取消指令,然后我们的监控 goroutine 就会收到信号,就会返回结束。


Context 控制多个 goroutine


使用 Context 控制一个 goroutine 的例子如上,非常简单,下面我们看看控制多个 goroutine 的例子,其实也比较简单。


func main() {
  ctx, cancel := context.WithCancel(context.Background())
  go watch(ctx,"【监控1】")
  go watch(ctx,"【监控2】")
  go watch(ctx,"【监控3】")
  time.Sleep(10 * time.Second)
  fmt.Println("可以了,通知监控停止")
  cancel()
  //为了检测监控过是否停止,如果没有监控输出,就表示停止了
  time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
  for {
    select {
    case <-ctx.Done():
      fmt.Println(name,"监控退出,停止了...")
      return
    default:
      fmt.Println(name,"goroutine监控中...")
      time.Sleep(2 * time.Second)
    }
  }
}


示例中启动了 3 个监控 goroutine 进行不断的监控,每一个都使用了 Context 进行跟踪,当我们使用 cancel 函数通知取消时,这 3 个 goroutine 都会被结束。这就是 Context 的控制能力,它就像一个控制器一样,按下开关后,所有基于这个 Context 或者衍生的子 Context 都会收到通知,这时就可以进行清理操作了,最终释放 goroutine,这就优雅的解决了 goroutine 启动后不可控的问题。

目录
相关文章
|
5月前
|
存储 SQL 安全
Golang底层原理剖析之上下文Context
Golang底层原理剖析之上下文Context
129 0
|
5月前
|
Linux Go
浅谈Golang上下文Context
浅谈Golang上下文Context
56 0
|
2月前
|
人工智能 Go
Golang 里的 context
Golang 里的 context
18 0
|
5月前
|
数据管理 Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第25天】Go语言中的`context`包在并发、网络请求和长任务中至关重要,提供取消、截止时间和元数据管理。本文探讨`context`基础,如`Background()`、`TODO()`、`WithCancel()`、`WithDeadline()`和`WithTimeout()`。常见问题包括不当传递、过度使用`Background()`和`TODO()`以及忽略错误处理。通过取消和超时示例,强调正确传递上下文、处理取消错误和设置超时以提高应用健壮性和响应性。正确使用`context`是构建稳定高效Go应用的关键。
87 1
|
5月前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
61 1
|
监控 安全 Go
Golang 语言中 Context 的使用方式
Golang 语言中 Context 的使用方式
44 0
|
5月前
|
监控 安全 Go
golang面试:golang中的context(四)
golang面试:golang中的context(四)
72 0
|
XML JSON Java
知识分享之Golang——Gin学习之context上下文的获取与使用(三)
知识分享之Golang篇是我在日常使用Golang时学习到的各种各样的知识的记录,将其整理出来以文章的形式分享给大家,来进行共同学习。欢迎大家进行持续关注。 知识分享系列目前包含Java、Golang、Linux、Docker等等。
620 0
知识分享之Golang——Gin学习之context上下文的获取与使用(三)
|
存储 SQL 安全
Golang 语言标准库 context 包控制 goroutine
Golang 语言标准库 context 包控制 goroutine
43 0
|
Go API 调度
No.14 Golang中Context你了解吗?(下)
No.14 Golang中Context你了解吗?