【Go专家编程——并发控制——三剑客】

并发控制

我们考虑这么一种场景,协程在A执行过程中需要创建子协程A1、A2、A3…An,协程创建完子协程后就等待子协程退出。

针对这种场景,Go提供了三种解决方案:

  • Channel:使用channel控制子协程
    • 优点:实现简单
  • WaitGroup:使用信号量机制控制子协程
    • 优点:子协程个数动态可调整
  • Context:使用上下文控制子协程
    • 优点:方便控制子协程派生出的孙子协程的控制

1. Channel

channel一般用于协程之间的通信,不过channel也可以用于并发控制。比如主协程启动N个子协程,主协程等待所有子协程退出后再继续后续流程。

场景示例:
下面的程序通过创建N个channel来管理N个协程,每个协程都有一个channel用于与父协程通信,父协程创建完所有协程后等待所有协程结束。

package main

import(
	"time"
	"fmt"
)

func Process(ch chan int) {
	// Do some work...
	time.Sleep(time.Second)
	ch <- 1 //在管道中写入一个元素表示当前协程已结束
}

func main(){
	// 创建一个10个元素的切片,元素类型为channel
	channels := make([]chan int,10)

	for i:=0;i<10;i++{
		channels[i] = make(chan int)//在切片中放入一个channel
		go Process(channels[i])//启动协程,传入一个管道用于通信
	}

	for i,ch := range channels{//遍历子协程结束
		<-ch
		fmt.Println("Routine",i," quit!")
	}
}

小结

优点:实现简单
缺点:需要创建与协程数量相等的channel,且对于子孙协程的控制不方便

2.WaitGroup

WaitGroup可以理解为Wait-Goroutine-Group,即等待一组goroutine结束。比如某个goroutine需要等待其他几个goroutine全部完成,那么使用WaitGroup可以轻松实现。

场景示例:

func main(){
	var wg sync.WaitGroup
	
	wg.Add(2)//设置计数器,数值即goroutine的个数
	go func(){
		//Do some work
		time.Sleep(1*time.Second)
		fmt.Println("Goroutine 1 finished!")
		wg.Done() //goroutine 执行结束后将计数器减1
	}()
	go func(){
		//Do some work
		time.Sleep(1*time.Second)
		fmt.Println("Goroutine 2 finished!")
		wg.Done() //goroutine 执行结束后将计数器减1
	}()
	wg.Wait() // 主goroutine阻塞等待计数器变为0
	fmt.Println("All Goroutine finished!")
}

2.1 基础知识

信号量是Unix系统提供的一种共享保护资源的机制,用于防止多个线程同时访问某个资源

  • 当信号量>0时,表示资源可用,获取信号量时系统自动将信号量减1
  • 当信号量==0时,表示资源不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒

2.2 WaitGroup

2.2.1 数据结构

type WaitGroup struct{
	state1 [3]uint32
}

state1 是一个长度为3的数组,包含state和一个信号量,而state实际上又分为两个计数器。

  • counter:当前还未执行结束的goroutine的计数器
  • waiter count:等待goroutine-group结束的goroutine数量,即又多少个等候者
  • semaphore:信号量

WaitGroup对外暴露三个方法

  • Add(delta int):将delta值加到counter中
  • Wait():waiter递增1,并阻塞等待信号量semaphore
  • Done():counter递减1,按照waiter数值释放相应次数信号量

2.2.2 Add(delta int)

Add方法做了两件事:

  1. 把delta值累加到counter中,因为delta可以为负值,也就是锁counter有可能变成0或负值
  2. 当counter变为0时,根据waiter数值释放等量信号量,把等待的goroutine全部唤醒,如果counter变为负值,则触发panic

2.2.3 Wait()

Wait方法做了两件事:

  1. 累加waiter
  2. 阻塞等待信号量

使用了CAS算法保证了多个goroutine同时执行Wait方法是也能正确累加waiter

2.2.4 Done()

Done方法只做一件事,即把counter减1,Done方法实际上调用的是Add(-1)。

3.Context

Go语言的context是应用开发常用的并发控制技术,对于派生goroutine有更强的控制力,可以控制多级的goroutine。

3.1 context的接口定义

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

基础的context接口只定义了4个方法

  • Deadline:返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,则ok=false,此时deadline为一个初始值的time.Time值
  • Done:该方法返回一个用来探测Context是否取消的channel,当Context取消时会自动将该channel关闭
    • 对于不支持取消的Context,该方法可能会返回nil
  • Err:返回context关闭的原因,若context还未关闭,返回nil
  • Value:
    • 适用于在协程中传递信息
    • 根据key值查询map中的value

3.2 空context

context包中定义了一个空的context,名为emptyCtx,用于context的根节点。
同时定义了一个公用的emptyCtx全局变量,名为background,可以使用context.Background()获取

context包中提供了四个方法创建不同类型的context,使用这四个方法时如果没有父节点context,需要传入background作为其父节点。

  • WithCancel()
  • WithDeadline()
  • WithTimeout()
  • WithValue()

3.3 cancelCtx

type cancelCtx struct{
	Context
	mu	sync.Mutex
	done	chan struct{}//lazily初始化
	//记录由此context派生出的所有child
	//此context被取消时会将其child全部cancel
	children	map[canceler]struct{}
	err	error
}

2.3.1 Done方法的实现

Done方法只需要返回一个channel接即可
源码如下:

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

2.3.2 Err方法的实现

Err()只需要返回一个error告知context被关闭的原理。
cancelCtx.err默认是nil,在context被”cancel“时指定一个error变量:
var Canceled = errors.New("context canceled")

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

2.3.3 cancel方法的实现

cancel方法是理解cancelCtx的关键,其作用是关闭自己和其后代。
伪代码如下:

func(c *cancelCtx) cancel(removeFromParent bool,err error){
	c.mu.Lock()
	
	c.err = err	//设置一个error,说明关闭原因
	close(c.done)	//将channel关闭,依次通知派生的context

	for child := range c.children{//遍历child,调用cancel方法
		child.cancel(false,err)	
	}
	c.children = nil
	c.mu.Unlock()
	if removeFromParent{
		//正常情况下,需要将自己从parent中删除
		removeChild(c.Context, c)
	}
}

2.3.4 WithCancel方法的实现

WithCancel()方法做了三件事

  • 初始化一个cancelCtx实例
  • 将cancelCtx实例添加到其父节点的children中(如果父节点也可以被cancel)
  • 返回cancelCtx实例和cancel方法

其实现源码如下:

func WithCancel(parent Context)(ctx Context,cancel CancelFunc){
	c:= newCancelCtx(parent)
	//将自身添加到支持cancel的祖先节点中,如果都不支持,则启动一个协程等待父节点结束,然后把当前context结束。
	propagateCancel(parent, &c)
	return &c, func(){c.cancel(true, Canceled)}
}

2.3.5 使用案例

func HandelRequest(ctx context.Context) {
	go WriteRedis(ctx)
	go WriteDatabase(ctx)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done.")
			return
		default:
			fmt.Println("HandelRequest running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteRedis(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteRedis Done.")
			return
		default:
			fmt.Println("WriteRedis running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteDatabase(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteDatabase Done.")
			return
		default:
			fmt.Println("WriteDatabase running")
			time.Sleep(2 * time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go HandelRequest(ctx)

	time.Sleep(5 * time.Second)
	fmt.Println("It's time to stop all sub goroutines")
	cancel()
	// Just for test whether sub goroutines exit or not
	time.Sleep(5 * time.Second)
}

main协程创建context,并把context在各个子协程间传递,main协程在适当的时机可以"cancel"所有子协程。
在这里插入图片描述

3.4 timerCtx

源码如下:

type timerCtx struct{
	cancelCtx
	timer *time.Timer
	deadline time.Time
}

timerCtx在cancel的基础上增加了deadline,用于标示自动cancel的最终时间,而timer就上触发自动cancel的定时器。
在此基础上衍生出了WithDeadline()和WithTimeout()

  • deadline:指定最后期限
  • timeout:指定最长存活时间

3.4.1 Deadline方法的实现

该方法仅仅返回timerCtx.deadline而已。

3.4.2 cancel方法的实现

cancel()方法基本继承了cancelCtx,只需要额外把timer关闭
timerCtx被关闭后,timerCtx.cancelCtx.err将存储关闭原因:

  • 如果在deadline到来之前手动关闭,则关闭原因与cancelCtx显示一致
  • 如果是到期自动关闭,原因为context deadline exceeded

3.4.3 WithDeadline方法的实现

  • 初始化一个timerCtx实例
  • 将timerCtx实例添加到其父节点的children中(如果父节点可以被“cancel”)
  • 启动定时器,定时器到期后会自动"cancel"本context
  • 返回timerCtx实例和cancel方法

3.4.4 WithTimeout方法的实现

WithTimeout()实际上调用了WithDeadline,两者的实现原理一致。

func WithTimeout(parent Context,timeout time.Duration)(Context,CancelFunc){
	return WithDeadline(parent,time.Now().Add(timeout))
}

3.4.5 典型使用案例

func HandelRequest(ctx context.Context) {
	go WriteRedis(ctx)
	go WriteDatabase(ctx)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done.")
			return
		default:
			fmt.Println("HandelRequest running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteRedis(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteRedis Done.")
			return
		default:
			fmt.Println("WriteRedis running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteDatabase(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteDatabase Done.")
			return
		default:
			fmt.Println("WriteDatabase running")
			time.Sleep(2 * time.Second)
		}
	}
}

func main() {
	ctx, _ := context.WithTimeout(context.Background(),5*time.Second)
	go HandelRequest(ctx)

	time.Sleep(10 * time.Second)
}

3.5 valueCtx

源码如下:

type valueCtx struct{
	Context
	key,value interface{}
}

valueCtx只是在Context基础上增加了一个key-value对,用于在各级协程间传递一些数据,由于valueCtx不需要cancel,也不需要deadline,那么只需要实现Value接口即可。

3.5.1 Value方法的实现

当前context查询不到key时,会向父节点查找,如果查询不到最终返回interface{}

func(c *valueCtx) Value(key interface{}) interface{}{
	if c.key == key{
		return c.val
	}
	return c.Context.Value(key)
}

3.5.2 WithValue方法的实现

伪代码如下:

func WithValue(parent Context,key,val interface{})Context{
	if key == nil {
		panic("nil key")
	}
	return &valueCtx{parent,key,val}
}

3.5.3 典型使用案例

该案例中main()通过WithValue()方法获得一个context,需要指定一个父context、key和value。contextkey用来在父子协程中传递信息。由于父节点是一个Timeout类型的,当父节点结束后,子节点也会结束。

func HandelRequest(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done.")
			return
		default:
			fmt.Println("HandelRequest running,parameter:",ctx.Value("parameter"))
			time.Sleep(2 * time.Second)
		}
	}
}


func main() {
	cancelCtx, _ := context.WithTimeout(context.Background(), 10*time.Second)
	ctx := context.WithValue(cancelCtx, "parameter", "1")
	go HandelRequest(ctx)

	time.Sleep(15 * time.Second)
}

在这里插入图片描述

4. 小结

三种context实例可以互为父节点,从而可以组合成不同的应用形式。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

6.1 Go 数组

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

Spring Cloud Alibaba-06-Sleuth链路追踪

Lison <dreamlison163.com>, v1.0.0, 2024.4.03 Spring Cloud Alibaba-06-Sleuth链路追踪 文章目录 Spring Cloud Alibaba-06-Sleuth链路追踪为什么使用链路追踪常见链路追踪解决方案Sleuth概述概述Sleuth术语 Sleuth Zipkin 原理Sleuth原理简述Zipkin 原理简述 Sleut…

永恒之蓝(MS17-010)详解

这个漏洞还蛮重要的&#xff0c;尤其在内网渗透和权限提升。 目录 SMB简介 SMB工作原理 永恒之蓝简原理 影响版本 漏洞复现 复现准备 复现过程 修复建议 SMB简介 SMB是一个协议服务器信息块&#xff0c;它是一种客户机/服务器、请求/响应协议&#xff0c;通过SMB协议…

[ARM-2D 专题] 1.开始:基本工程搭建,编译和开发环境配置问题解决

要开始使用ARM-2D&#xff0c;前期两个准备工作需要完成&#xff1a; 一块mcu内核为cortex-M的板子&#xff0c;带显示屏&#xff08;彩色TFT屏&#xff0c;分辨率建议320x240或以上&#xff0c;带TP更佳&#xff09;。基于这个板子可以正常运行的keil MDK的工程。 好了&#…

科技守护,河流水文监测保障水资源安全!

中小河流是城乡水资源的补给&#xff0c;又是不可或缺的排放渠道&#xff0c;维系着城乡水资源的平衡与生态的健康。然而&#xff0c;随着工业化、城市化的快速推进&#xff0c;河流生态环境面临着越来越大的压力。为了有效保护和合理利用河流资源&#xff0c;河流水文监测成为…

vs code 中使用SSH 连接远程的Ubuntu系统

如下图&#xff0c;找到对应的位置 在电脑上找到以下位置 打开配置如下&#xff0c;记住&#xff0c;那个root为你的用户名&#xff0c;这个用户名&#xff0c;具体根据你的用户名来设置&#xff0c;对应的密码就是你登录Ubuntu时的密码 Host root192.168.0.64User rootHostNa…

第98天:权限提升-WIN 全平台MSF 自动化CS 插件化EXP 筛选溢出漏洞

目录 思维导图 前置知识 案例一&#xff1a; Web&Win2008-人工手动&全自动msf-筛选&下载&利用 手动 全自动msf 案例二: Web&Win2019-CS 半自动-反弹&插件&利用 思维导图 前置知识 提权方式&#xff0c;这里讲的是溢出漏洞 windows权限 常…

实时合成 1 秒频订单簿快照:DolphinDB INSIGHT 行情插件与订单簿引擎应用

INSIGHT 是华泰证券依托大数据存储、实时分析等领域的技术积累&#xff0c;整合接入国内多家交易所高频行情数据&#xff0c;为投资者提供集行情接入、推送、回测、计算及分析等功能于一体的行情数据服务解决方案。基于 INSIGHT 官方提供的行情数据服务 C SDK&#xff08;TCP 版…

HTML静态网页成品作业(HTML+CSS)——动漫海贼王介绍网页(1个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有1个页面。 二、作品演示 三、代…

超分论文走读

codeFormer 原始动机 高度不确定性&#xff0c;模糊到高清&#xff0c;存在一对多的映射纹理细节丢失人脸身份信息丢失 模型实现 训练VQGAN 从而得到HQ码本空间作为本文的离散人脸先验。为了降低LQ-HQ映射之间的不确定性&#xff0c;我们设计尽量小的码本空间和尽量短的Code…

人人都是产品经理,尼恩产品经理面试宝典(史上最全、定期更新)

《人人都是产品经理&#xff0c;尼恩产品经理面试宝典》&#xff08;史上最全、定期更新&#xff09; 本文版本说明&#xff1a;V1 IT不老新物种 的定义 大龄男IT &#xff1a;APM 架构经理 项目经理 高级开发&#xff0c;没有中年危机 大龄女IT&#xff1a;DPM 产品经理 …

Simulink从0搭建模型07-P8for循环的使用

Simulink从0搭建模型07-P8for循环的使用 今日学习内容1. For Iterator Subsystem模块介绍1.1. 累加器1.2. For Iterator1.3.小结 2. states介绍3. Set next i&#xff08;相当break)学习心得 今日学习内容 b站视频 【Simulink 0基础入门教程 P8 for循环的使用 For Itrator Sub…

Power Bi 自定义进度条,圆角框,矩阵图标的实现

最近项目在做Power BI&#xff0c;我总结了几个常用的自定义样式&#xff0c;分享一下做法。 比如我们要实现如图这样的一个样式&#xff1a; 这包含了一个带文字的自定义进度条&#xff0c;矩阵有树型展开以及图标显示&#xff0c;最外面有圆角框包围。我觉得这几个样式出现…

ATA-2021B高压放大器在锂电池超声检测中的应用

锂电池一种高能量密度的电池&#xff0c;已经广泛应用于可穿戴设备、移动电话、笔记本电脑和电动汽车等领域中。然而&#xff0c;其在使用过程中存在着一定的安全隐患&#xff0c;锂电池内部的化学反应和充放电过程可能会导致电池发热&#xff0c;甚至发生燃烧。Aigtek安泰电子…

走进数字艺术的世界:一种创新的艺术表达方式

进入数字时代&#xff0c;计算机将我们生活的方方面面都进行了转化。当然艺术领域也不例外。随着数字技术和计算机程序的发展和普及&#xff0c;“数字艺术”的概念应试而生。那么&#xff0c;所谓的数字艺术到底是什么呢&#xff1f;数字艺术的作用是什么&#xff1f;新手如何…

在项目中集成Web端数据库操作:推荐工具一览

在项目中集成Web端数据库操作&#xff1a;推荐工具一览 博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍…

基于分块贝叶斯非局部均值优化(OBNLM)的图像去噪算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1 块定义与相似度计算 ​4.2 贝叶斯框架下的加权融合 4.3 加权最小均方误差估计 5.完整程序 1.程序功能描述 基于分块贝叶斯非局部均值优化(OBNLM)的图像去噪算法matlab仿真&#xff0c…

CASS+arcgis实现图斑的分割

1、在CASS中将图形绘制好&#xff0c;待分割图形为闭合线&#xff0c;使用线段将其分割成很多块&#xff0c;如下&#xff1a; 2、保存文件。打开arcgisPro&#xff0c;加载dwg图形&#xff0c;如下&#xff1a; 效果如下&#xff1a; 3、分别将面和线导出&#xff0c;如下&…

FL Studio21.2.5中文版电子音乐制作的强大工具

在当今的数字音乐时代&#xff0c;电子音乐已经成为了全球音乐市场中不可或缺的一部分。越来越多的音乐爱好者开始尝试自己动手创作电子音乐&#xff0c;而FL Studio 21中文版正是为他们量身打造的一款强大工具。作为一个音频制作爱好者&#xff0c;我深知一个好的数字音频工作…

LeetCode热题100 Day1——双指针

双指针 移动零11. 盛最多水的容器 移动零 思路&#xff1a; 双指针i&#xff0c;j&#xff0c;j指针遍历数组&#xff0c;i指针存放非0元素。遍历结束后&#xff0c;i指针及其后面的一定是0,就再将空出来的位置设置为0 移动零 class Solution {public void moveZeroes(int[] …