go当中的channel 无缓冲channel和缓冲channel的适用场景、结合select的使用

Channel

Go channel就像Go并发模型中的“胶水”,它将诸多并发执行单元连接起来,或者正是因为有channel的存在,Go并发模型才能迸发出强大的表达能力。

无缓冲channel

无缓冲channel兼具通信和同步特性,在并发程序中应用颇为广泛。

可以通过不带有capacity参数的内置make函数创建一个可用的无缓冲channel:

c := make(chan T)

由于无缓冲channel的运行时层实现不带有缓冲区,因此对无缓冲channel的接收和发送操作是同步的。

一个无缓冲的channel动作发生和完成的时序如下:

  • 发送动作一定发生在接收动作完成之前;
  • 接收动作一定发生在发送动作完成之前。

这与Go官方“Go内存模型”一文中对channel通信的描述是一致的。正因如此,下面的代码可以保证main输出的变量a的值为"hello, world":

var c = make(chan int)
var a stringfunc f() {a = "hello, world"<-c
}func main() {go f()c <- 5println(a)
}

因为函数f中的channel接收动作发生在主goroutine对channel发送动作完成之前,而a = “hello, world"语句又发生在channel接收动作之前,因此主goroutine在channel发送操作完成后看到的变量a的值一定是"hello, world”,而不是空字符串。

用作信号传递

1)一对一通知信号

无缓冲channel常被用于在两个goroutine之间一对一地传递通知信号,

(2)一对多通知信号

无缓冲channel还被用来实现一对多的信号通知机制

关闭一个无缓冲channel会让所有阻塞在该channel上的接收操作返回,从而实现一种一对多的广播机制。该一对多的信号通知机制还常用于通知一组worker goroutine退出:

// 通知其他goroutine工作线程已完成
type signal struct{}func worker(i int) {fmt.Printf("worker %v is working\n", i)time.Sleep(5 * time.Second)fmt.Printf("worker %v : works done\n", i)
}// spawnGroup 是一个用于生成一组工作线程的函数
// 参数:
//   - f: 工作函数,每个工作线程都会执行该函数
//   - num: 工作线程的数量
//   - groupSignal: 用于控制工作线程启动和停止的信号通道
//
// 返回值:
//   - <-chan signal: 用于接收所有工作线程完成的信号通道
func spawnGroup(f func(i int), num int, groupSignal <-chan signal) <-chan signal {c := make(chan signal)var wg sync.WaitGroupfor i := 0; i < num; i++ {wg.Add(1)go func(i int) {<-groupSignal //阻塞,等待启动信号fmt.Printf("worker %v: start to work\n", i)f(i)wg.Done() //工作完成,减少WaitGroup的计数}(i + 1)}go func() {wg.Wait()c <- signal(struct{}{}) //发送信号通知}()return c
}func main() {fmt.Println("start a group of workers...")groupSignal := make(chan signal)c := spawnGroup(worker, 5, groupSignal)time.Sleep(10 * time.Second)fmt.Println("the group of workers start to work...")// 关闭工作组信号通道,通知所有工作线程开始工作close(groupSignal)<-cfmt.Println("the group of workers work done!")
}

最后结果:

Untitled

用于替代锁机制

由于无缓冲channel具有同步特性,因此可以在某些场合替代锁,让程序更加清晰,可读性增强;以下给出基于共享内存+锁模式的goroutine安全的计数器:

type counter struct {c chan inti int
}var cter counterfunc InitCounter() {cter = counter{c: make(chan int),}// 增加计数器的动作相当于一次接收动作go func() {for {cter.i++cter.c <- cter.i}}()fmt.Println("counter init ok")
}func Increase() int {return <-cter.c
}func init() {InitCounter()
}func main() {for i := 0; i < 10; i++ {go func(i int) {v := Increase()fmt.Printf("goroutine-%d: current counter value is %d\n", i, v)}(i)}time.Sleep(5 * time.Second)
}

此代码通过无缓冲channel的同步阻塞特性实现计数器的控制

也符合“不要通过共享内存来通信,而应该通过通信来共享内存”的原则

缓冲channel

带缓冲channel可以通过带有capacity参数的内置make函数创建

c := make(chan T, capacity)

接收操作在缓冲区非空的情况下是异步的(发送或接收无须阻塞等待)

用作消息队列

channel的原生特性与我们认知中的消息队列十分相似,包括goroutine安全、有fifo(first-in, first out)保证等。异步收发的带缓冲channel更适合用作消息队列,并且带缓冲channel在数据收发性能上要明显好于无缓冲channel

用作计数信号量 counting semaphore

带缓冲channel当前数据个数代表的是同时处于活跃状态的goroutine数量,capacity则代表同时处于活跃状态的最大数量。以下是一个例子:

// 同一时间最多3个活跃状态
var active = make(chan struct{}, 3)
var jobs = make(chan int, 10)func main() {go func() {for i := 0; i < 8; i++ {jobs <- i + 1}close(jobs)}()var wg sync.WaitGroupfor j := range jobs {wg.Add(1)go func(j int) {active <- struct{}{}log.Printf("handle job: %v\n", j)time.Sleep(2 * time.Second)<-activewg.Done()}(j)}wg.Wait()
}

结果:

Untitled

可以发现同一时间处于处理状态的job最多为3个

len(channel)的应用

如果s是chan T类型,那么len(s)针对channel的类型不同,有如下两种语义:

  • 当s为无缓冲channel时,len(s)总是返回0;
  • 当s为带缓冲channel时,len(s)返回当前channel s中尚未被读取的元素个数。

但是单纯依靠if语句来判断channel元素状态并不可靠,因为在并发状态下不能保证后续对channel进行收发时channel状态不变:

Untitled

oroutine1在使用len(channel)判空后,便尝试从channel中接收数据。但在其真正从channel中读数据前,goroutine2已经将数据读了出去,goroutine1后面的读取将阻塞在channel上,导致后面逻辑失效。因此,为了不阻塞在channel上,常见的方法是将判空与读取放在一个事务中,将判满与写入放在一个事务中,而这类事务我们可以通过select实现。来看下面的示例:

func producer(c chan<- int) {var i int = 1for {time.Sleep(2 * time.Second)ok := trySend(c, i)if ok {fmt.Printf("[producer]: send [%d] to channel\n", i)i++continue}fmt.Printf("[producer]: try send [%d], but channel is full\n", i)}
}func tryRecv(c <-chan int) (int, bool) {select {case i := <-c:return i, truedefault:return 0, false}
}func trySend(c chan<- int, i int) bool {select {case c <- i:return truedefault:return false}
}func consumer(c <-chan int) {for {i, ok := tryRecv(c)if !ok {fmt.Println("[consumer]: try to recv from channel, but the channel is empty")time.Sleep(1 * time.Second)continue}fmt.Printf("[consumer]: recv [%d] from channel\n", i)if i >= 3 {fmt.Println("[consumer]: exit")return}}
}func main() {c := make(chan int, 3)go producer(c)go consumer(c)select {} // 仅用于演示,临时用来阻塞主goroutine
}

结果:

Untitled

这种方法的缺点就在于改变了channel的状态

想在不改变channel状态的前提下单纯地侦测channel的状态,又不会因channel满或空阻塞在channel上。但很遗憾,目前没有一种方法既可以实现这样的功能又适用于所有场合。在特定的场景下,可以用len(channel)来实现。比如图34-2中的这两种场景。

在图34-2中,a是一个多发送单接收的场景,即有多个发送者,但有且只有一个接收者。在这样的场景下,我们可以在接收者goroutine中根据len(channel)是否大于0来判断channel中是否有数据需要接收。

b是一个多接收单发送的场景,即有多个接收者,但有且只有一个发送者。在这样的场景下,我们可以在发送goroutine中根据len(channel)是否小于cap(channel)来判断是否可以执行向channel的发送操作。

Untitled

nil channel的妙用

没有初始化的channel(nil channel)进行读写操作将会发生阻塞

func main() {var c chan int<-c
}

结果:

Untitled

main goroutine被阻塞在channel上,导致Go运行时认为出现deadlock状态并抛出panic。

但nil channel并非一无是处。来看一个例子:

func main() {c1, c2 := make(chan int), make(chan int)go func() {time.Sleep(time.Second * 5)c1 <- 5close(c1)}()go func() {time.Sleep(time.Second * 7)c2 <- 7close(c2)}()for {select {case x, ok := <-c1:// 对于一个nil channel执行获取操作,该操作会被堵塞,因此可以显示设置if !ok {c1 = nil} else {fmt.Println(x)}case x, ok := <-c2:if !ok {c2 = nil} else {fmt.Println(x)}}if c1 == nil && c2 == nil {break}}fmt.Println("program end")
}

与select结合

避免阻塞

default的使用通常是在没得选的情况下,因此也有一种可以避免堵塞的特性

func sendTime(c interface{}, seq uintptr) {// 无阻塞地向c发送当前时间// ...select {case c.(chan Time) <- Now():default:}
}

实现超时机制

通过超时事件,既可以避免陷入无尽的等待也可以做一些异常处理工作:

func worker() {select {case <-c://...case <-time.After(30*time.Second):return}
}

timer实质上是由go运行时自动维护的,而不是操作系统的定时器资源:

Untitled

go通过名为timerproc的函数,维护了一个“最小堆”。该goroutine会被定期唤醒并读取堆顶的timer对象,执行该timer对象对应的函数(向timer.C中发送一条数据,触发定时器),执行完毕后就会从最小堆中移除该timer对象。

所以我们在使用timer的时候应该即使调用timer的Stop方法从最小堆中删除尚未到达过期时间的timer对象。

实现心跳机制

这种机制可以使我们在监听的同时执行一些周期性任务,比如下面这段代码:

func worker() {heartbeat := time.NewTicker(30 * time.Second)defer heartbeat.Stop()for {select {case <-c:// ... 处理业务逻辑case <- heartbeat.C: //记得调用方法停止运作//... 处理心跳}}
}

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

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

相关文章

坚鹏:贵州银行西南财经大学零售业务数字化转型与场景营销策略

中国银保监会2022年1月正式发布了中国银保监会发布《关于银行业保险业数字化转型的指导意见》&#xff0c;这标准着中国银行业从局部的数字化转型向全面的数字化转型转变&#xff0c;进一步加速了银行数字化转型高潮的到来。 《关于银行业保险业数字化转型的指导意见》提出明确…

【教学类-06-12】20231126 (二)三位数 如何让加减乘除题目从小到大排序(以0-110之间加法为例,做正序排列用)

结果展示 背景需求&#xff1a; 二位数&#xff1a;去0 三位数&#xff08;需要排除很多0&#xff09; 解决思路 一、把数字改成三位数 二、对数组内的题目&#xff0c;8种可能性进行去“0”处理 1、十位数&#xff08;去百位数0&#xff09;十位数&#xff08;去百位数0&am…

数据增强让模型更健壮

在做一些图像分类训练任务时,我们经常会遇到一个很尴尬的情况,那就是: 明明训练数据集中有很多可爱猫咪的照片,但是当我们给训练好的模型输入一张戴着头盔的猫咪进行测试时,模型就不认识了,或者说识别精度很低。 很明显,模型的泛化能力太差,难道戴着头盔的猫咪就不是猫…

线性分类器--数据处理

数据集划分 通常按照 70%&#xff0c;20% &#xff0c;10% 来分数据集 数据处理 斯坦福的线性分类器体验 http://vision.stanford.edu/teaching/cs231n-demos/linear-classify/

【解决视觉引导多个位置需要标定多个位置的问题】

** 以下只针对2D定位&#xff0c;就是只有X、Y、Rz三个自由度的情况。** 假设一种情况&#xff0c;当视觉给机器人做引导任务时&#xff0c;零件有多个&#xff0c;分布在料框里&#xff0c;视觉需要走多个位置去拍&#xff0c;那么只需要对第一个位置确定拍照位&#xff0c;确…

美SEC与贝莱德,对比特币现货ETF申购方式产生分歧!

比特币现货ETF的通过时间是市场投资者密切关注的议题。虽然SEC最近推迟了Hashdex、富兰克林邓普顿&#xff08;Franklin Templeton&#xff09;和GlobalX申请的决议时间&#xff0c;但彭博ETF分析师James Seyffart对明年一月通过的机率持乐观态度&#xff0c;认为其通过的机会能…

Java - Stream Filter 多条件筛选过滤

Java Stream流中Filter用于通过设置的条件过滤出元素 &#xff0c;示例如下&#xff1a; List strings Arrays.asList(“abc”, “”, “bc”, “efg”, “abcd”,"", “jkl”);List filtered strings.stream().filter(string -> !string.isEmpty()).collect(C…

eclipse - jee 建立项目后没有 web.xml

eclipse -- jee 建立项目后没有 web.xml 处理它的方法是&#xff0c;点 File - New - Dynamic Web Project , 此时起一个项目名如M4 然后next 然后next 出现如此所示:

基于springboot实现农机电招平台系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现农机电招平台系统演示 摘要 随着农机电招行业的不断发展&#xff0c;农机电招在现实生活中的使用和普及&#xff0c;农机电招行业成为近年内出现的一个新行业&#xff0c;并且能够成为大群众广为认可和接受的行为和选择。设计农机电招平台的目的就是借助计算…

论文阅读:“Model-based teeth reconstruction”

文章目录 AbstractIntroductionTeeth Prior ModelData PreparationParametric Teeth Model Teeth FittingTeeth Boundary Extraction Reference Abstract 近年来&#xff0c;基于图像的人脸重建方法日趋成熟。这些方法可以捕捉整个面部或面部特定区域&#xff08;如头发、眼睛…

CAN总线星型连接器及特点

CAN总线星型连接特点 CAN总线是一种广泛应用于汽车、工业自动化、家庭等领域的现场总线技术。它具有高速度、高可靠性、灵活性等特点&#xff0c;被广泛应用于汽车电子、工业自动化、家庭自动化等领域。在CAN总线的实际应用中&#xff0c;其连接方式可以是星型或菊花型。本文将…

Leetcode—160.相交链表【简单】

2023每日刷题&#xff08;四十一&#xff09; Leetcode—160.相交链表 算法思想 两个链表的节点之和是相等的 如果两个链表相交&#xff0c;那么相交点之后的长度是相同的 我们需要做的事情是&#xff0c;让两个链表从同距离末尾同等距离的位置开始遍历。这个位置只能是较短…

Linux系统编程:文件系统总结

目录和文件 获取文件属性 获取文件属性有如下的系统调用&#xff0c;下面逐个来分析。 stat:通过文件路径获取属性&#xff0c;面对符号链接文件时获取的是所指向的目标文件的属性 从上图中可以看到stat函数接收一个文件的路径字符串&#xff08;你要获取哪个文件的属性&a…

第二十三章 解析PR曲线、ROC曲线、AUC、AP(工具)

混淆矩阵Confusion Matrix 混淆矩阵定义 混淆矩阵是机器学习中总结分类模型预测结果的情形分析表&#xff0c;以矩阵形式将数据集中的记录按照真实的类别与分类模型预测的类别判断两个标准进行汇总。其中矩阵的行表示真实值&#xff0c;矩阵的列表示预测值&#xff0c;下面我…

file_get_contents() 函数详解与使用

概述 在PHP中&#xff0c;file_get_contents() 函数是一个强大的工具&#xff0c;它既可以用于读取本地文件的内容&#xff0c;也可以用于发起 HTTP 请求获取远程资源。本文将详细介绍 file_get_contents() 函数的两种主要用途&#xff0c;并探讨如何充分利用这个函数。 1. 文…

【高效开发工具系列】MapStruct入门使用

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

数据结构与算法编程题30

层次遍历二叉树(队列&#xff1a;先进先出) #define _CRT_SECURE_NO_WARNINGS#include <iostream> using namespace std;typedef char ElemType; #define ERROR 0 #define OK 1 #define Maxsize 100 #define STR_SIZE 1024typedef struct BiTNode {ElemType data;BiTNode…

Sringboot3 讲解

文章目录 前言一、Springboot快速入门1.1 实例1.2 总结&#xff1a;1.2.1 什么是starter启动器1.2.2 SpringBootApplication注解的功效 二、springboot3 统一配置文件1.概述2、属性配置文件使用简单案例3、yaml配置介绍和说明4、批量配置文件的读取5、多环境配置和激活 三、spr…

Netty I/O模型和线程模型

目录 1.概述 1.1 为什么使用Netty 1.2 Netty的优势 1.3 Netty的常见使用场景 2.Netty高性能的原因 2.1 I/O模型 2.1.1 阻塞IO 2.1.2 IO复用模型 2.2 线程模型 2.2.1 线程模型1&#xff1a;传统阻塞 I/O 服务模型 2.2.2 线程模型2&#xff1a;Reactor 模式 2.2.2.1 …

Javaweb之Vue组件库Element之Dialog对话框的详细解析

4.3.3 Dialog对话框 4.3.3.1 组件演示 Dialog: 在保留当前页面状态的情况下&#xff0c;告知用户并承载相关操作。其企业开发应用场景示例如下图所示 首先我们需要在ElementUI官方找到Dialog组件&#xff0c;如下图所示&#xff1a; 然后复制如下代码到我们的组件文件的templ…