Go语言并发之context标准库

这篇具有很好参考价值的文章主要介绍了Go语言并发之context标准库。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、Go语言并发之context标准库

Go中的 goroutine 之间没有父与子的关系,也就没有所谓子进程退出后的通知机制,多个 goroutine 都是平行地

被调度,多个 goroutine 如何协作工作涉及通信、同步、通知和退出四个方面。

  • 通信:chan 通道当然是 goroutine 之间通信的基础,注意这里的通信主要是指程序的数据通道。

  • 同步:不带缓冲的 chan 提供了一个天然的同步等待机制;当然 sync.WaitGroup 也为多个 goroutine 协同工

    作提供一种同步等待机制。

  • 通知:这个通知和上面通信的数据不一样,通知通常不是业务数据,而是管理、控制流数据。要处理这个也好

    办,在输入端绑定两个 chan,一个用于业务流数据,另一个用于异常通知数据,然后通过 select 收敛进行处

    理。这个方案可以解决简单的问题,但不是一个通用的解决方案。

  • 退出:goroutine 之间没有父子关系,如何通知 goroutine 退出?可以通过增加一个单独的通道,借助通道和

    select 的广播机制实现退出。

Go语言在语法上处理某个 goroutine 退出通知机制很简单。但是遇到复杂的并发结构处理起来就显得力不从心。

实际编程中 goroutine 会拉起新的 goroutine,新的 goroutine 又会拉起另一个新的 goroutine,最终形成一个树

状的结构,由于 goroutine 里并没有父子的概念,这个树状的结构只是在程序员头脑中抽象出来的,程序的执行

模型并没有维护这么一个树状结构。怎么通知这个树状上的所有 goroutine 退出?仅依靠语法层面的支持显然比

较难处理。为此 Go1.7 提供了一个标准库 context 来解决这个问题。它提供两种功能:退出通知和元数据传递。

context 库的设计目的就是跟踪 goroutine 调用,在其内部维护一个调用树,并在这些调用树中传递通知和元数

据。

1.1 Context的设计目的

context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。两个目

的:

(1)、退出通知机制:通知可以传递给整个goroutine 调用树上的每一个goroutine。

(2)、传递数据:数据可以传递给整个goroutine 调用树上的每一个goroutine。

1.2 基本数据结构

在介绍 context 包之前,先理解context包的整体工作机制:第一个创建Context的goroutine被称为root节点。

root节点负责创建一个实现Context接口的具体对象,并将该对象作为参数传递到其新拉起的goroutine,下游的

goroutine可以继续封装该对象,再传递到更下游的goroutine。

Context 对象在传递的过程中最终形成一个树状的数据结构,这样通过位干root节点(树的根节点)的Context 对象

就能遍历整个Context 对象树,通知和消息就可以通过root节点传递出去,实现了上游 goroutine 对下游

goroutine的消息传递。

1.2.1 Context接口

Context 是一个基本接口,所有的 Context 对象都要实现该接口,context 的使用者在调用接口中都使用 Context

作为参数类型。

type Context interface{
	
    // 如果Context实现了超时控制,则该方法返回ok true,deadline为超时时间,否则ok为false
	Deadline() (deadline time.Time,ok bool)
	
    // 后端被调的goroutine应该监听该方法返回的chan,以便及时释放资源
    Done() <-chan struct{}
	
    // Done返回的chan收到通知的时候,才可以访问Err()获知因为什么原因被取消
	Err()error
	
    // 可以访问上游 goroutine 传递给下游 goroutine 的值
	Value(key interface{}) interface{}
}
1.2.2 canceler接口

canceler 接口是一个扩展接口,规定了取消通知的 Context 具体类型需要实现的接口。

context 包中的具体类型 cancelctx 和 timerCtx 都实现了该接口。

1.2.3 empty Context结构

emptyCtx 实现了 Context 接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为

Context 对象树的根(root 节点)。因为context包的使用思路就是不停地调用context 包提供的包装函数来创建具有

特殊功能的Context 实例,每一个Context 实例的创建都以上一个Context对象为参数,最终形成一个树状的结

构。

1.2.4 cancelCtx

cancelCtx 是一个实现了Context接口的具体类型,同时实现了conceler接口。conceler具有退出通知方法。注意

退出通知机制不但能通知己,也能逐层通知其children 节点。

1.2.5 timerCtx

timerCtx 是一个实现了Context接口的具体类型,内部封装了cancelCtx类型实例,同时有一个 deadline 变量,

用来实现定时退出通知。

1.2.6 valueCtx

valueCtx 是一个实现了Context接口的具体类型,内部封装了Context接口类型,同时封装了一个k/v的存储变

量,valueCtx 可用来传递通知信息。

1.3 API函数

下面这两个函数是构造 Context 取消树的根节点对象,根节点对象用作后续 With 包装函数的实参。

func Background() Context
func TODO() Context

With 包装函数用来构建不同功能的 Context 具体对象。

(1)、创建一个带有退出通知的 Context 具体对象,内部创建一个 cancelCtx 的类型实例。

func WithCancel(parent Context)(Context,CancelFunc)

(2)、创建一个带有超时通知的 Context 具体对象,内部创建一个 timerCtx 的类型实例。

func WithDeadline(parent Context,deadline time.Time)(Context,CancelFunc)

(3)、创建一个带有超时通知的 Context 具体对象,内部创建一个 timerCtx 的类型实例。

func WithTimeout(parent Context,timeout time.Duration)(Context,CancelFunc)

(4)、创建一个能够传递数据的 Context 具体对象,内部创建一个 valueCtx 的类型实例。

func WithValue(parent Context,key,val interface{})Context

这些函数都有一个共同的特点—— parent 参数,其实这就是实现 Context 通知树的必备条件。在 goroutine 的

调用链中,Context 的实例被逐层地包装并传递,每层又可以对传讲来的 Context 实例再封装自己所需的功能,

整个调用树需要一个数据结构来维护,这个维护逻辑在这些包装函数内部实现。

1.4 辅助函数

前面描述的 With 开头的构造函数是给外部程序使用的 API 接口函数。Context 具体对象的链条关系是在 With 函

数的内部维护的。

func propagateCancel(parent Context,child canceler)

1.5 context的用法

package main

import (
	"context"
	"fmt"
	"time"
)

// 定义一个新的类型包含Context字段
type otherContext struct {
	context.Context
}

func main() {
	// 创建一个带有退出通知的Context具体对象:*cancelCtx
	ctxA, cancel := context.WithCancel(context.Background())
	go work(ctxA, "work1")
	// 超时3秒计算
	tm := time.Now().Add(3 * time.Second)
	// 创建一个带有超时通知的Context具体对象:*timerCtx
	ctxB, _ := context.WithDeadline(ctxA, tm)
	go work(ctxB, "work2")
	oc := otherContext{ctxB}
	// 创建一个能够传递数据的Context具体对象:*cancelCtx
	ctxC := context.WithValue(oc, "key", "god andes,pass from main ")
	go workWithValue(ctxC, "work3")
	time.Sleep(10 * time.Second)
	cancel()
	// 这5秒没有什么作用
	time.Sleep(5 * time.Second)
	fmt.Println("main stop")
}

// 工作
func work(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			fmt.Printf("%s is running \n", name)
			time.Sleep(1 * time.Second)
		}
	}
}

// 根据context传递值
func workWithValue(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			value := ctx.Value("key").(string)
			fmt.Printf("%s is running value=%s \n", name, value)
			time.Sleep(1 * time.Second)
		}
	}
}
# 程序输出
work3 is running value=god andes,pass from main
work1 is running
work2 is running
work1 is running
work3 is running value=god andes,pass from main
work2 is running
work3 is running value=god andes,pass from main
work2 is running
work1 is running
work1 is running
work3 get msg to cancel
work2 get msg to cancel
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
work1 get msg to cancel
main stop

上面的输出中:

work1 is running会输出10次,因为会休眠10秒。

work2 is running会输出3次,因为超时时间为3秒。

work3 is running也会输出3次,因为它的context取决于work2的context。

在使用 Context 的过程中,程序在底层实际上维护了两条关系链,理解这个关系链对理解 context 包非常有好

处,两条引用关系链如下。

(1)、children key 构成从根到叶子 Context 实例的引用关系,这个关系在调用 With 函数时进行维护(调用上文

介绍的 propagateCancel(parent Context,child canceler) 函数维护),程序有一层这样的树状结构(本示例

是一个链表结构):

ctxa.children--->ctxb
ctxb.children--->ctxc

这个树提供一种从根节点开始遍历树的方法,context 包的取消广播通知的核心就是基于这一点实现的。取消通

知沿着这条链从根节点向下层节点逐层广播。当然也可以在任意一个子树上调用取消通知,一样会扩散到整棵树。

示例程序中 ctxa 收到退出通知,会通知其绑定 work1,同时会广播给 ctxb 和 ctxc 绑定的 work2 和 work3。同

理,ctxb 收到退出通知,会通知到其绑定的 work2,同时会广播给 ctxc 绑定的 work3。

(2)、在构造 Context 的对象中不断地包裹 Context 实例形成一个引用关系链,这个关系链的方向是相反的,是自

底向上的。示例程序中多个 Context 对象的关系如下:

# 自底向上
ctxc.Context -->oc
ctxc.Context.Context -->ctxb
ctxc.Context.Context.cancelCtx-->ctxa
ctxc.Context.Context.cancelCtx.Context-->new(emptyCtx)//context.Background()

这个关系链主要用来切断当前 Context 实例和上层的 Context 实例之间的关系,比如 ctxb 调用了退出通知或定时

器到期了,ctxb 后续就没有必要在通知广播树上继续存在,它需要找到自己的 parent,然后执行

delete(parent.children,ctxb),把自己从广播树上清理掉。

整个关系链如图所示:

Go语言并发之context标准库

通过上文示例梳理出使用 Context 包的一般流程如下:

(1)、创建一个 Context 根对象。

func Background() Context
func TODO() Context

(2)、包装上一步创建的 Context 对象,使其具有特定的功能。

这些包装函数是 context package 的核心,几乎所有的封装都是从包装函数开始的。原因很简单,使用 context

包的核心就是使用其退出通知广播功能。

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

(3)、将上一步创建的对象作为实参传给后续启动的并发函数(通常作为函数的第一个参数),每个并发函数内部可以

继续使用包装函数对传进来的 Context 对象进行包装,添加自己所需的功能。

(4)、顶端的 goroutine 在超时后调用 cancel 退出通知函数,通知后端的所有 goroutine 释放资源。

(5)、后端的 goroutine 通过 select 监听 Context.Done() 返回的 chan,及时响应前端 goroutine 的退出通知,

一般停止本次处理,释放所占用的资源。

1.6 使用context传递数据的争议

该不该使用context传递数据:

首先要清楚使用 context 包主要是解决 goroutine 的通知退出,传递数据是其一个额外功能。可以使用它传递一

些元信息,总之使用 context 传递的信息不能影响正常的业务流程,程序不要期待在 context 中传递一些必需的

参数等,没有这些参数,程序也应该能正常工作。

在context中传递数据的坏处:

(1)、传递的都是 interface{} 类型的值,编译器不能进行严格的类型校验。

(2)、从 interface{} 到具体类型需要使用类型断言和接口查询,有一定的运行期开销和性能损失。

(3)、值在传递过程中有可能被后续的服务覆盖,且不易被发现。

(4)、传递信息不简明,较晦涩;不能通过代码或文档一眼看到传递的是什么,不利于后续维护。

context应该传递什么数据:

(1)、日志信息。

(2)、调试信息。

(3)、不影响业务主逻辑的可选数据。

context 包提供的核心的功能是多个 goroutine 之间的退出通知机制,传递数据只是一个辅助功能,应谨慎使用

context 传递数据。

1.7 Context常用案例

1.7.1 主协程主动调用cancel()取消子context
package main

import (
	"context"
	"fmt"
	"time"
)

func handler() {
	fmt.Println("handler start...", time.Now())
	ctx, cancel := context.WithCancel(context.Background())
	go do(ctx)
	// 子孙携程中的ctx会被取消
	cancel()
	time.Sleep(5 * time.Second)
	fmt.Println("handler end...", time.Now())
}

func do(ctx context.Context) {
	i := 1
	for {
		time.Sleep(1 * time.Second)
		select {
		case <-ctx.Done():
			fmt.Println("done", time.Now())
			return
		default:
			fmt.Printf("work %d seconds: %v\n", i, time.Now())
		}
		i++
	}
}

func main() {
	fmt.Println("main start...")
	handler()
	fmt.Println("main end...")
}
# 程序输出
main start...
handler start... 2023-02-06 12:18:29.3556787 +0800 CST m=+0.002016401
done 2023-02-06 12:18:30.3665436 +0800 CST m=+1.012881301
handler end... 2023-02-06 12:18:34.365822 +0800 CST m=+5.012159701
main end...

通过输出我们可以看出来,在主协程调用了 cancel() 之后,子协程中的 ctx 会被主动关闭掉,延迟时间是1秒,

会看到打印 done。

package main

import (
	"context"
	"fmt"
	"time"
)

func handler() {
	fmt.Println("handler start...", time.Now())
	ctx, _ := context.WithCancel(context.Background())
	go do(ctx)
	// 子孙携程中的ctx会被取消
	time.Sleep(5 * time.Second)
	fmt.Println("handler end...", time.Now())
}

func do(ctx context.Context) {
	i := 1
	for {
		time.Sleep(1 * time.Second)
		select {
		case <-ctx.Done():
			fmt.Println("done", time.Now())
			return
		default:
			fmt.Printf("work %d seconds: %v\n", i, time.Now())
		}
		i++
	}
}

func main() {
	fmt.Println("main start...")
	handler()
	fmt.Println("main end...")
}
# 程序输出
main start...
handler start... 2023-06-10 10:49:42.9315016 +0800 CST m=+0.002174101
work 1 seconds: 2023-06-10 10:49:43.9404127 +0800 CST m=+1.011085201
work 2 seconds: 2023-06-10 10:49:44.941524 +0800 CST m=+2.012196501
work 3 seconds: 2023-06-10 10:49:45.9422753 +0800 CST m=+3.012947801
work 4 seconds: 2023-06-10 10:49:46.9426627 +0800 CST m=+4.013335201
handler end... 2023-06-10 10:49:47.9421403 +0800 CST m=+5.012812801
main end...

从结果看出,我们如果不执行 cancel(),则会在设置的5秒睡眠时间内执行 work。

1.7.2 超时之后,调用cancle()
package main

import (
	"context"
	"fmt"
	"time"
)

func handler() {
	fmt.Println("handler start...", time.Now())
	// 设置2秒的超时时间
	ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
	go do(ctx)
	time.Sleep(5 * time.Second)
	fmt.Println("handler end...", time.Now())
}

func do(ctx context.Context) {
	i := 1
	for {
		time.Sleep(1 * time.Second)
		select {
		case <-ctx.Done():
			fmt.Println("done", time.Now())
			return
		default:
			fmt.Printf("work %d seconds: %v\n", i, time.Now())
		}
		i++
	}
}

func main() {
	fmt.Println("main start...")
	handler()
	fmt.Println("main end...")
}
# 程序输出
main start...
handler start... 2023-06-10 10:51:17.0507657 +0800 CST m=+0.002568201
work 1 seconds: 2023-06-10 10:51:18.0618317 +0800 CST m=+1.013634201
done 2023-06-10 10:51:19.0620425 +0800 CST m=+2.013845001
handler end... 2023-06-10 10:51:22.0630702 +0800 CST m=+5.014872701
main end...

通过输出可以看出来,在2s超时之后,也就是done会主动打印出来,表明 cancel() 被主动调用了。文章来源地址https://www.toymoban.com/news/detail-493050.html

到了这里,关于Go语言并发之context标准库的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 掌握Go并发:Go语言并发编程深度解析

    🏷️ 个人主页 :鼠鼠我捏,要死了捏的主页  🏷️ 系列专栏 :Golang全栈-专栏 🏷️ 个人学习笔记,若有缺误,欢迎评论区指正   前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站AI学习网站。 当我们开发一个W

    2024年02月20日
    浏览(69)
  • Go语言math标准库

    参考资料 函数 功能 备注 NaN() float64 返回一个IEEE 754“这不是一个数字”值 IsNaN(f float64) (is bool) 报告f是否表示一个NaN(Not A Number)值 Inf(sign int) float64 如果sign=0函数返回正无穷大,否则返回负无穷大 IsInf(f float64, sign int) bool 如果sign 0,f是正无穷大时返回真;如果sign0,f是负无

    2024年02月08日
    浏览(37)
  • Go语言并发

    出色的并发性是Go语言的特色之一 • 理解并发与并行 • 理解进程和线程 • 掌握Go语言中的Goroutine和channel • 掌握select分支语句 • 掌握sync包的应用 并发与并行的概念这里不再赘述, 可以看看之前java版写的并发实践; 程序、进程与线程这里也不赘述 一个进程可以包括多个线

    2024年02月06日
    浏览(30)
  • 【Go】Go语言并发编程:原理、实践与优化

    在当今的计算机世界,多核处理器和并发编程已经成为提高程序执行效率的关键。Go语言作为一门极富创新性的编程语言,凭借其强大的并发能力,在这方面表现出色。本文将深入探讨Go语言并发编程的原理,通过实际代码示例展示其应用,并讨论可能的优化策略。 在了解G

    2024年02月10日
    浏览(59)
  • GO语言网络编程(并发编程)并发介绍,Goroutine

    进程和线程 并发和并行 协程和线程 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。 线程:一个线程上可以跑多个协程,协程是轻量级的线程。 goroutine 只是由官方实现的超级\\\"线程池\\\"。 每个

    2024年02月09日
    浏览(58)
  • 掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)

    通道(Channel)是用来在 Go 程序中传递数据的一种数据结构。它是一种类型安全的、并发安全的、阻塞式的数据传输方式,用于在不同的 Go 协程之间传递消息。 基本概念 创建通道 :使用 make() 函数创建一个通道。 发送数据 :使用 - 操作符向通道发送数据。 接收数据 :使用

    2024年03月21日
    浏览(59)
  • Go语言并发模式视角思考

    犹记得2019年中旬进行知识点的学习和demo的练习,熟悉各种语法和并发调度的场景, 在2019年末开始参与项目实战开发和逻辑梳理 Go语言的接触也是更多探索和业务的拆件,做一些雏形工具,来慢慢的孵化业务生态 后来陆陆续续,在主营业务是PHP的情况下,尽量在业务脚本的

    2024年01月21日
    浏览(51)
  • Go语言并发之WaitGroup

    goroutine 和 chan,一个用于并发,另一个用于通信。没有缓冲的通道具有同步的功能,除此之外,sync 包也提 供了多个 goroutine 同步的机制,主要是通过 WaitGroup 实现的。 WaitGroup 用来等待多个 goroutine 完成,main goroutine 调用 Add 设置需要等待 goroutine 的数目,每一个 goroutine 结束

    2024年02月08日
    浏览(36)
  • Go 语言面试题(三):并发编程

    对于无缓冲的 channel,发送方将阻塞该信道,直到接收方从该信道接收到数据为止,而接收方也将阻塞该信道,直到发送方将数据发送到该信道中为止。 对于有缓存的 channel,发送方在没有空插槽(缓冲区使用完)的情况下阻塞,而接收方在信道为空的情况下阻塞。 例如: 协

    2024年02月08日
    浏览(51)
  • Go语言并发编程(千锋教育)

    视频地址:https://www.bilibili.com/video/BV1t541147Bc?p=14 作者B站:https://space.bilibili.com/353694001 源代码:https://github.com/rubyhan1314/go_goroutine 1.1、并发与并行 其实操作系统里对这些概念都有所说明和举例。 并发 并发是指多个任务在同一时间段内交替执行,从外部看似乎是同时执行的。

    2024年02月14日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包