Go语言,并发控制神器之Context

简介: 一个协程启动后,一般是代码执行完毕,自动退出,但是如果需要提前终止怎么办呢?一个办法是定义一个全局变量,协程中通过检查这个变量的变化来决定是否退出。这种办法须要加锁来保证并发安全

协程如何退出

一个协程启动后,一般是代码执行完毕,自动退出,但是如果需要提前终止怎么办呢?

一个办法是定义一个全局变量,协程中通过检查这个变量的变化来决定是否退出。这种办法须要加锁来保证并发安全,说到这里,有没有想的什么解决方案?

select + channel 来实现:

package main
import (
  "fmt"
  "sync"
  "time"
)
func main() {
  var wg sync.WaitGroup
  stopWk := make(chan bool)
  wg.Add(1)
  go func() {
    defer wg.Done()
    worker(stopWk)
  }()
  time.Sleep(3*time.Second) //工作3秒
  stopWk <- true //3秒后发出停止指令
  wg.Wait()
}
func worker(stopWk chan bool){
  for {
    select {
    case <- stopWk:
      fmt.Println("下班咯~~~")
      return
    default:
      fmt.Println("认真摸鱼中,请勿打扰...")
    }
    time.Sleep(1*time.Second)
  }
}
复制代码

运行结果:

认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
下班咯~~~
复制代码

可以看到,每秒打印一次“认真摸鱼中,请勿打扰...”,3秒后发出停止指令,程序进入 “下班咯~~~”。

Context 初体验

上面我们使用 select+channel 来实现了协程的终止,但是如果我们想要同时取消多个协程怎么办呢?如果需要定时取消又怎么办呢?

此时,Context 就需要登场了,它可以跟踪每个协程,我们重写上面的示例:

package main
import (
  "context"
  "fmt"
  "sync"
  "time"
)
func main() {
  var wg sync.WaitGroup
  ctx, stop := context.WithCancel(context.Background())
  wg.Add(1)
  go func() {
    defer wg.Done()
    worker(ctx)
  }()
  time.Sleep(3*time.Second) //工作3秒
  stop() //3秒后发出停止指令
  wg.Wait()
}
func worker(ctx context.Context){
  for {
    select {
    case <- ctx.Done():
      fmt.Println("下班咯~~~")
      return
    default:
      fmt.Println("认真摸鱼中,请勿打扰...")
    }
    time.Sleep(1*time.Second)
  }
}
复制代码

运行结果:

认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
下班咯~~~
复制代码

Context 介绍

Context 是并发安全的,它是一个接口,可以手动、定时、超时发出取消信号、传值等功能,主要是用于控制多个协程之间的协作、取消操作。

Context 接口有四个方法:

type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}
复制代码
  • Deadline 方法:可以获取设置的截止时间,返回值 deadline 是截止时间,到了这个时间,Context 会自动发起取消请求,返回值 ok 表示是否设置了截止时间。
  • Done 方法:返回一个只读的 channel ,类型为 struct{}。如果这个 chan 可以读取,说明已经发出了取消信号,可以做清理操作,然后退出协程,释放资源。
  • Err 方法:返回Context 被取消的原因。
  • Value 方法:获取 Context 上绑定的值,是一个键值对,通过 key 来获取对应的值。

最常用的是 Done 方法,在 Context 取消的时候,会关闭这个只读的 Channel,相当于发出了取消信号。

Context 树

我们并不需要自己去实现 Context 接口,Go 语言提供了函数来生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 就可以关联起来,父级 Context 发出取消信号,子级 Context 也会发出,这样就可以控制不同层级的协程退出。

生成根节点

  1. emptyCtx是一个int类型的变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为 context 树的根节点。
  2. 但是我们一般不直接使用emptyCtx,而是使用由emptyCtx实例化的两个变量(background 、todo),分别通过调用BackgroundTODO方法得到,但这两个 context 在实现上是一样的。

Background和TODO方法区别:

BackgroundTODO只是用于不同场景下:Background通常被用于主函数、初始化以及测试中,作为一个顶层的context,也就是说一般我们创建的context都是基于Background;而TODO是在不确定使用什么context的时候才会使用。

生成树的函数

  1. 可以通过 context。Background() 获取一个根节点 Context。
  2. 有了根节点后,再使用以下四个函数来生成 Context 树:
  • WithCancel(parent Context):生成一个可取消的 Context。
  • WithDeadline(parent Context, d time.Time):生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。
  • WithTimeout(parent Context, timeout time.Duration):生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消
  • WithValue(parent Context, key, val interface{}):生成一个可携带 key-value 键值对的 Context。

Context 取消多个协程

如果一个 Context 有子 Context,在该 Context 取消时,其下的所有子 Context 都会被取消。

image.png

Context 传值

Context 不仅可以发出取消信号,还可以传值,可以把它存储的值提供其他协程使用。

示例:

package main
import (
  "context"
  "fmt"
  "sync"
  "time"
)
func main() {
  var wg sync.WaitGroup
  ctx, stop := context.WithCancel(context.Background())
  valCtx := context.WithValue(ctx, "position","gopher")
  wg.Add(2)
  go func() {
    defer wg.Done()
    worker(valCtx, "打工人1")
  }()
  go func() {
    defer wg.Done()
    worker(valCtx, "打工人2")
  }()
  time.Sleep(3*time.Second) //工作3秒
  stop() //3秒后发出停止指令
  wg.Wait()
}
func worker(valCtx context.Context, name string){
  for {
    select {
    case <- valCtx.Done():
      fmt.Println("下班咯~~~")
      return
    default:
      position := valCtx.Value("position")
      fmt.Println(name,position, "认真摸鱼中,请勿打扰...")
    }
    time.Sleep(1*time.Second)
  }
}
复制代码

运行结果:

打工人2 gopher 认真摸鱼中,请勿打扰...
打工人1 gopher 认真摸鱼中,请勿打扰...
打工人1 gopher 认真摸鱼中,请勿打扰...
打工人2 gopher 认真摸鱼中,请勿打扰...
打工人2 gopher 认真摸鱼中,请勿打扰...
打工人1 gopher 认真摸鱼中,请勿打扰...
下班咯~~~
下班咯~~~
复制代码

Context 使用原则

  • Context 不要放在结构体中,需要以参数方式传递
  • Context 作为函数参数时,要放在第一位,作为第一个参数
  • 使用 context。Background 函数生成根节点的 Context
  • Context 要传值必要的值,不要什么都传
  • Context 是多协程安全的,可以在多个协程中使用


相关文章
|
5天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
3天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
12 2
|
3天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
14 2
|
6天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
3天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
10 4
|
3天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
14 1
|
7天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
7天前
|
Go 数据处理 调度
探索Go语言的并发模型:Goroutines与Channels的协同工作
在现代编程语言中,Go语言以其独特的并发模型脱颖而出。本文将深入探讨Go语言中的Goroutines和Channels,这两种机制如何协同工作以实现高效的并发处理。我们将通过实际代码示例,展示如何在Go程序中创建和管理Goroutines,以及如何使用Channels进行Goroutines之间的通信。此外,本文还将讨论在使用这些并发工具时可能遇到的常见问题及其解决方案,旨在为Go语言开发者提供一个全面的并发编程指南。
|
5天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
16天前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
32 3