context.WithCancel()的使用

news2024/11/19 23:25:42

WithCancel可以将一个Context包装为cancelCtx,并提供一个取消函数,调用这个取消函数,可以Cancel对应的Context

Go语言context包-cancelCtx[1]


疑问


context.WithCancel()取消机制的理解[2]

父母5s钟后出门,倒计时,父母在时要学习,父母一走就可以玩

package main

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

func dosomething(ctx context.Context) {
 for {
  select {
  case <-ctx.Done():
   fmt.Println("playing")
   return
  default:
   fmt.Println("I am working!")
   time.Sleep(time.Second)
  }
 }
}

func main() {
 ctx, cancelFunc := context.WithCancel(context.Background())
 go func() {
  time.Sleep(5 * time.Second)
  cancelFunc()
 }()
 dosomething(ctx)
}
alt

为什么调用cancelFunc就能从ctx.Done()里取得返回值? 进而取消对应的Context?


复习一下channel的一个特性


从一个已经关闭的channel里可以一直获取对应的零值

alt

WithCancel代码分析


pkg.go.dev/context#WithCancel:[3]

// 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.

//WithCancel 返回具有新 Done 通道的 parent 副本。 返回的上下文的完成通道在调用返回的取消函数或父上下文的完成通道关闭时关闭,以先发生者为准。

//取消此上下文会释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
 if parent == nil {
  panic("cannot create context from nil parent")
 }
 c := newCancelCtx(parent)   // 将parent作为父节点context 生成一个新的子节点

 //获得“父Ctx路径”中可被取消的Ctx
 //将child canceler加入该父Ctx的map中
 propagateCancel(parent, &c)
 return &c, func() { c.cancel(true, Canceled) }
}

WithCancel最后返回 子上下文和一个cancelFunc函数,而cancelFunc函数里调用了cancelCtx这个结构体的方法cancel

(代码基于go 1.16; 1.17有所改动)

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
 Context

 mu       sync.Mutex            // protects following fields
 done     chan struct{}         // created lazily, closed by first cancel call done是一个channel,用来 传递关闭信号
 children map[canceler]struct{} // set to nil by the first cancel call  children是一个map,存储了当前context节点下的子节点
 err      error                 // set to non-nil by the first cancel call  err用于存储错误信息 表示任务结束的原因
}

在cancelCtx这个结构体中,字段done是一个传递空结构体类型的channel,用来在上下文取消时关闭这个通道,err就是在上下文被取消时告诉用户这个上下文取消了,可以用ctx.Err()来获取信息

canceler是一个实现接口,用于Ctx的终止。实现该接口的Context有cancelCtx和timerCtx,而emptyCtx和valueCtx没有实现该接口。

alt
// 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{}
}

// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})

func init() {
 close(closedchan)
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
/**
* 1、cancel(...)当前Ctx的子节点
* 2、从父节点中移除该Ctx
**/

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 = err

 //  设置一个关闭的channel或者将done channel关闭,用以发送关闭信号
 if c.done == nil {
  c.done = closedchan
 } else {
  close(c.done) // 注意这一步
 }

  // 将子节点context依次取消
 for child := range c.children {
  // NOTE: acquiring the child's lock while holding parent's lock.
  child.cancel(false, err)
 }
 c.children = nil
 c.mu.Unlock()

 if removeFromParent {
   // 将当前context节点从父节点上移除
  removeChild(c.Context, c)
 }
}

对于cancel函数,其取消了基于该上下文的所有子上下文以及把自身从父上下文中取消

对于更多removeFromParent代码分析,和其他Context的使用,强烈建议阅读 深入理解Golang之Context(可用于实现超时机制)[4]


 // Done is provided for use in select statements:
 //
 //  // Stream generates values with DoSomething and sends them to out
 //  // until DoSomething returns an error or ctx.Done is closed.
 //  func Stream(ctx context.Context, out chan<- Value) error {
 //   for {
 //    v, err := DoSomething(ctx)
 //    if err != nil {
 //     return err
 //    }
 //    select {
 //    case <-ctx.Done():
 //     return ctx.Err()
 //    case out <- v:
 //    }
 //   }
 //  }
 //
 // See https://blog.golang.org/pipelines for more examples of how to use
 // a Done channel for cancellation.
 Done() <-chan struct{}

 // If Done is not yet closed, Err returns nil.
 // If Done is closed, Err returns a non-nil error explaining why:
 // Canceled if the context was canceled
 // or DeadlineExceeded if the context's deadline passed.
 // After Err returns a non-nil error, successive calls to Err return the same error.
 Err() error

当调用cancelFunc()时,会有一步close(d)的操作,

ctx.Done 获取一个只读的 channel,类型为结构体。可用于监听当前 channel 是否已经被关闭。

Done()用来监听cancel操作(对于cancelCtx)或超时操作(对于timerCtx),当执行取消操作或超时时,c.done会被close,这样就能从一个已经关闭的channel里一直获取对应的零值<-ctx.Done便不会再阻塞

(代码基于go 1.16; 1.17有所改动)

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

func (c *cancelCtx) Err() error {
 c.mu.Lock()
 err := c.err
 c.mu.Unlock()
 return err
}

总结一下:使用context.WithCancel时,除了返回一个新的context.Context(上下文),还会返回一个cancelFunc。 在需要取消该context.Context时,就调用这个cancelFunc,之后当前上下文及其子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号

至于cancelFunc是如何做到的?

在用户代码,for循环里select不断尝试从 <-ctx.Done()里读取出内容,但此时并没有任何给 c.done这个channel写入数据的操作,(类似c.done <- struct{}{}),故而在for循环里每次select时,这个case都不满足条件,一直阻塞着。每次都执行default代码段

而在执行cancelFunc时, 在func (c *cancelCtx) cancel(removeFromParent bool, err error)里面,会有一个close(c.done)的操作。而从一个已经关闭的channel里可以一直获取对应的零值,即 select可以命中,进入case res := <-ctx.Done():代码段


可用如下代码验证:

package main

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

func dosomething(ctx context.Context) {

 var cuiChan = make(chan struct{})

 go func() {
  cuiChan <- struct{}{}
 }()

 //close(cuiChan)

 for {
  select {
  case res := <-ctx.Done():
   fmt.Println("res:", res)
   return
  case res2 := <-cuiChan:
   fmt.Println("res2:", res2)
  default:
   fmt.Println("I am working!")
   time.Sleep(time.Second)
  }
 }
}

func main() {

 test()
 ctx, cancelFunc := context.WithCancel(context.Background())
 go func() {
  time.Sleep(5 * time.Second)
  cancelFunc()
 }()

 dosomething(ctx)
}

func test() {

 var testChan = make(chan struct{})

 if testChan == nil {
  fmt.Println("make(chan struct{})后为nil")
 } else {
  fmt.Println("make(chan struct{})后不为nil!!!")
 }

}

输出:

make(chan struct{})后不为nil!!!
I am working!
res2: {}
I am working!
I am working!
I am working!
I am working!
res: {}

而如果 不向没有缓存的cuiChan写入数据,直接close,即

package main

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

func dosomething(ctx context.Context) {

 var cuiChan = make(chan struct{})

 //go func() {
 // cuiChan <- struct{}{}
 //}()

 close(cuiChan)

 for {
  select {
  case res := <-ctx.Done():
   fmt.Println("res:", res)
   return
  case res2 := <-cuiChan:
   fmt.Println("res2:", res2)
  default:
   fmt.Println("I am working!")
   time.Sleep(time.Second)
  }
 }
}

func main() {

 test()
 ctx, cancelFunc := context.WithCancel(context.Background())
 go func() {
  time.Sleep(5 * time.Second)
  cancelFunc()
 }()

 dosomething(ctx)
}

func test() {

 var testChan = make(chan struct{})

 if testChan == nil {
  fmt.Println("make(chan struct{})后为nil")
 } else {
  fmt.Println("make(chan struct{})后不为nil!!!")
 }

}

则会一直命中case 2

res2: {}
res2: {}
res2: {}
res2: {}
res2: {}
res2: {}
res2: {}
...
//一直打印下去

更多参考:

深入理解Golang之Context(可用于实现超时机制)[5]

回答我,停止 Goroutine 有几种方法?

golang context的done和cancel的理解 for循环channel实现context.Done()阻塞输出[6]




更多关于channel阻塞与close的代码


package main

import (
 "fmt"
 "time"
)

func main() {
 ch := make(chan string0)
 go func() {
  for {
   fmt.Println("----开始----")
   v, ok := <-ch
   fmt.Println("v,ok", v, ok)
   if !ok {
    fmt.Println("结束")
    return
   }
   //fmt.Println(v)
  }
 }()

 fmt.Println("<-ch一直没有东西写进去,会一直阻塞着,直到3秒钟后")
 fmt.Println()
 fmt.Println()
 time.Sleep(3 * time.Second)

 ch <- "向ch这个channel写入第一条数据..."
 ch <- "向ch这个channel写入第二条数据!!!"

 close(ch) // 当channel被close后, v,ok 中的ok就会变为false

 time.Sleep(10 * time.Second)
}

输出为:

----开始----
<-ch一直没有东西写进去,会一直阻塞着,直到3秒钟后


v,ok 向ch这个channel写入第一条数据... true
----开始----
v,ok 向ch这个channel写入第二条数据!!! true
----开始----
v,ok  false
结束


package main

import (
 "fmt"
 "sync/atomic"
 "time"
)

func main() {
 ch := make(chan string0)
 done := make(chan struct{})

 go func() {
  var i int32

  for {
   atomic.AddInt32(&i, 1)
   select {
   case ch <- fmt.Sprintf("%s%d%s""第", i, "次向通道中写入数据"):

   case <-done:
    close(ch)
    return
   }

   // select随机选择满足条件的case,并不按顺序,所以打印出的结果,在30几次波动
   time.Sleep(100 * time.Millisecond)
  }
 }()

 go func() {
  time.Sleep(3 * time.Second)
  done <- struct{}{}
 }()

 for i := range ch {
  fmt.Println("接收到的值: ", i)
 }

 fmt.Println("结束")
}

输出为:

接收到的值:  第1次向通道中写入数据
接收到的值:  第2次向通道中写入数据
接收到的值:  第3次向通道中写入数据
接收到的值:  第4次向通道中写入数据
接收到的值:  第5次向通道中写入数据
接收到的值:  第6次向通道中写入数据
接收到的值:  第7次向通道中写入数据
接收到的值:  第8次向通道中写入数据
接收到的值:  第9次向通道中写入数据
接收到的值:  第10次向通道中写入数据
接收到的值:  第11次向通道中写入数据
接收到的值:  第12次向通道中写入数据
接收到的值:  第13次向通道中写入数据
接收到的值:  第14次向通道中写入数据
接收到的值:  第15次向通道中写入数据
接收到的值:  第16次向通道中写入数据
接收到的值:  第17次向通道中写入数据
接收到的值:  第18次向通道中写入数据
接收到的值:  第19次向通道中写入数据
接收到的值:  第20次向通道中写入数据
接收到的值:  第21次向通道中写入数据
接收到的值:  第22次向通道中写入数据
接收到的值:  第23次向通道中写入数据
接收到的值:  第24次向通道中写入数据
接收到的值:  第25次向通道中写入数据
接收到的值:  第26次向通道中写入数据
接收到的值:  第27次向通道中写入数据
接收到的值:  第28次向通道中写入数据
接收到的值:  第29次向通道中写入数据
接收到的值:  第30次向通道中写入数据
接收到的值:  第31次向通道中写入数据
结束

每次执行,打印出的结果,在30几次波动

参考资料

[1]

Go语言context包-cancelCtx: https://dashen.tech/2019/06/23/Go%E8%AF%AD%E8%A8%80context%E5%8C%85/#cancelCtx

[2]

context.WithCancel()取消机制的理解: https://blog.csdn.net/weixin_42216109/article/details/123694275

[3]

pkg.go.dev/context#WithCancel:: https://pkg.go.dev/context#WithCancel

[4]

深入理解Golang之Context(可用于实现超时机制): https://blog.csdn.net/qq_25821689/article/details/105850717

[5]

深入理解Golang之Context(可用于实现超时机制): https://blog.csdn.net/qq_25821689/article/details/105850717

[6]

golang context的done和cancel的理解 for循环channel实现context.Done()阻塞输出: https://blog.csdn.net/nakeer/article/details/120897267

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/945894.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

yo!这里是Linux进程入门介绍

目录 前言 os定位 进程基本介绍 进程基本操作 查看进程 创建进程 进程状态 理论层面 具体状态 进程优先级 环境变量 地址空间 进程地址空间&&页表介绍 深入理解地址空间 后记 前言 在了解学习过Linux环境下的基本操作以及工具之后&#xff0c;就来到了…

本地套接字通信

1.本地套接字 本地套接字的作用&#xff1a;本地的进程间通信 有关系的进程间的通信 没有关系的进程间的通信 本地套接字实现流程和网络套接字类似&#xff0c;一般采用TCP的通信流程 2.本地套接字通信的流程 - tcp // 服务器端 1.创建监听的套接字int lfd socket(AF_U…

聊聊中南亚跨境电商的机遇与前景

随着工业转移&#xff0c;中南亚地区作为一个充满潜力的区域&#xff0c;正逐渐成为跨境电商领域的热点。这一地区包括印度、孟加拉国、巴基斯坦、斯里兰卡等国家&#xff0c;拥有庞大的人口、不断增长的中产阶级&#xff0c;以及逐步完善的数字基础设施&#xff0c;为跨境电商…

Android投屏总结

#android手机投屏 ####导语 至于手机投屏的实现方法可谓五花八门&#xff0c;今天小袁就说下以开发人员的角度来说下当今手机的主流投屏方法。目前这种将终端信号经由WiFi传输到电视、电视盒的技术有三种&#xff1a;DLNA、AirPlay、Miracast、Google Cast。 ##手机投屏智能电…

for...in和for...of有什么区别

在JavaScript中遍历数组通常是使用for...i循环&#xff0c;在ES5具有遍历数组功能的还有forEach、map、filter、some、every、reduce、reduceRight等。 for...in和for...of是两种增强型循环&#xff0c;for...in是ES5标准&#xff0c;在ES6中新增了for...of的循环方式。 1.fo…

基于ADAU1452 DSP语音信号处理算法系统构建

是否需要申请加入数字音频系统研究开发交流答疑群(课题组)&#xff1f;加我微信hezkz17, 本群提供音频技术答疑服务&#xff0c;群赠送音频&#xff0c;DSP音频项目核心开发资料 1 LMS, NLMS 最小均方自适应滤波算法 2 语音活动检测&#xff0c;去混响&#xff0c;波束形成算…

字符设备驱动读写操作实现

一、读操作实现 ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos); 完成功能&#xff1a;读取设备产生的数据 参数&#xff1a;filp&#xff1a;指向open产生的struct file类型的对象&#xff0c;表示本次read对应的那次openpbuf&#xf…

十四五双碳双控时代下的“低碳认证”

目录 前言 十四五双碳双控时代下的“低碳认证” 一、关于“低碳认证” 二、低碳认证优势 三、环境产品认证EPD 四、EPD相关运营机构 五、碳中和相关机构 六、EPD的认证流程 七、低碳产品认证认证流程和要求 八、相关机构认证证书样例 九、证书附件表 前言 通过本篇文…

JavaWeb JQuery操作结点

目录 一、查找结点 1.简介 : 2.实例 : 二、创建结点 1.创建结点介绍 : 2.内部插入 : 2.1 介绍 : 2.2 实例 3.外部插入 : 3.1 介绍 3.2 实例 三、结点的其他相关操作 1.删除结点 &#xff1a; 2.复制结点 : 3.替换结点 : 4.遍历结点 : 一、查找结点 1.简介 : 查找到…

RAID磁盘阵列(RAID0/1/4/6/1+0)

目录 一、概述&#xff1a; 二、RAID 级别介绍 RAID 0 RAID 1 RAID 4 RAID 5 RAID 6 RAID10&#xff1a; 一、概述&#xff1a; RAID&#xff08; Redundant Array of Inexpensive Disks&#xff09;称为廉价磁盘冗余阵列。 RAID 的基本思想是把多个便宜的小磁盘组合到…

编写简单的支付接口测试用例思路

支付接口Python代码示例 以下是一个简单的支付接口Python代码示例&#xff0c;实现了用户登录后购买商品并支付的功能&#xff1a; import requests import json # 定义支付接口的URL和请求参数 url "https://example.com/pay" payment_data {"username&quo…

【业务功能篇87】微服务-springcloud-本地缓存-redis-分布式缓存-缓存穿透-雪崩-击穿

一、缓存 1. 什么是缓存 缓存的作用是减低对数据源的访问频率。从而提高我们系统的性能。 缓存的流程图 2.缓存的分类 2.1 本地缓存 其实就是把缓存数据存储在内存中(Map <String,Object>).在单体架构中肯定没有问题。 单体架构下的缓存处理 2.2 分布式缓存 在分布式环…

Iterator: hasNext()、next()、remove()

一、Iterator的API 关于Iterator主要有三个方法&#xff1a;hasNext()、next()、remove() hasNext:没有指针下移操作&#xff0c;只是判断是否存在下一个元素next&#xff1a;指针下移&#xff0c;返回该指针所指向的元素remove&#xff1a;删除当前指针所指向的元素&#xf…

树莓派 SSD1306

树莓派安装python3.9以及pip换源_树莓派安装pip_&#xff2c;&#xff2a;&#xff38;的博客-CSDN博客 树莓派使用 Python 驱动 SSD1306&#xff08;IIC/SPI 通信&#xff09; 进阶篇——树莓派OLED模块的使用 大量例程详解_oled例程_玩转智能机器人的博客-CSDN博客 使用OS 版…

从零开始学习盲盒小程序开发

在当前的电商市场中&#xff0c;微信盲盒小程序以其独特的互动性和惊喜感&#xff0c;已经成为吸引消费者的一种新兴电商模式。如果你也想搭建自己的微信盲盒小程序&#xff0c;本文将详细介绍如何从零开始实现这一目标。 第一步&#xff1a;登录【乔拓云】网后台&#xff0c;进…

自然语言处理2-NLP

目录 自然语言处理2-NLP 如何把词转换为向量 如何让向量具有语义信息 在CBOW中 在Skip-gram中 skip-gram比CBOW效果更好 CBOW和Skip-gram的算法实现 Skip-gram的理想实现 Skip-gram的实际实现 自然语言处理2-NLP 在自然语言处理任务中&#xff0c;词向量&#xff08;…

利用fsimage分析HDFS小文件

一、Hive 小文件概述 在Hive中&#xff0c;所谓的小文件是指文件大小远小于HDFS块大小的文件&#xff0c;通常小于128 MB&#xff0c;甚至更少。这些小文件可能是Hive表的一部分&#xff0c;每个小文件都包含一个或几个表的记录&#xff0c;它们以文本格式存储。 Hive通常用于…

卫星网络中的量子通信

当今社会&#xff0c;通信已经成为人类生活中不可或缺的一部分&#xff0c;而随着科技的迅猛发展&#xff0c;我们的通信方式也在不断革新和进化。近年来&#xff0c;量子通信作为一项引人瞩目的领域&#xff0c;正逐渐走入人们的视野。与传统通信方式相比&#xff0c;量子通信…

渗透测试漏洞原理之---【任意文件上传漏洞】

文章目录 1、任意文件上传概述1.1、漏洞成因1.2、漏洞危害 2、WebShell解析2.1、Shell2.2、WebShell2.2.1、大马2.2.2、小马2.2.3、GetShell 3、任意文件上传攻防3.1、毫无检测3.1.1、源代码3.1.2、代码审计3.1.3、靶场试炼 3.2、黑白名单策略3.2.1、文件检测3.2.2、后缀名黑名…

【AI】数学基础——线代(矩阵特征值,特征向量矩阵分解)

【AI】数学基础——线代&#xff08;向量部分&#xff09; 文章目录 2.3 矩阵2.3.1 二元方程组求解与行列式行列式 2.3.2 用矩阵形式表示数据矩阵与行列式区别特殊矩阵 2.3.3 矩阵的秩矩阵的秩 2.3.4 矩阵运算加减法数乘运算矩阵乘向量线性变换角度线性组合角度 矩阵乘矩阵转置…