一文搞懂Goroutine之间的通信Channel

news2025/5/18 17:04:01

文章目录

    • Channel
      • 定义通道
      • 初始化通道
      • 通道的操作
        • 1、发送/接收
        • 2、关闭
      • 多返回值模式
      • for range获取通道值
    • 单向通道
    • select
    • goroutine、channel案例

道阻且长,行则将至,行而不辍,未来可期🌟。人生是一条且漫长且充满荆棘的道路,一路上充斥着各种欲望与诱惑,不断学习,不断修炼,不悔昨日,不畏将来!
GO语言也被称为21世纪的C语言,在开发与性能效率上都占据优势(Python+C)🚀。让我们一起来了解这门语言的魅力吧,希望这篇能够带给你们或多或少的帮助!!

在这里插入图片描述

Channel

通常一些语言是可以通过共享内存来实现不同线程间的数据交换,但是容易出现数据不正确的问题,一般需要通过互斥锁来确保数据的安全性,这时候就会出现性能问题

Go语言采用的是并发模型是(CSP),提倡"通过通信共享内存",而不是"通过共享内存实现通信",如果说goroutine是Go程序并发的执行体,那么channel则是它们之前的连接。channel是可以让一个goroutine发送一个特定的值到另外一个goroutine的通信机制

  • 通过共享内存实现通信
  • 通过通信共享内存共享(Go语言并发通信)

我们也可以理解:channel是G之间的通信的通道

定义通道

通道是chan类型的,并且在定义通道时需要指定通道传递的数据类型

var ch chan int // 定义一个可传递int类型的通道

未初始化的channel,默认值是nil

初始化通道

需要初始化之后才能使用,通道类型需要通过内置函数"make"进行初始化才能使用

make(chan type[缓冲区大小])

缓冲区:通道能存储的元素数量(类似数组),如果缓冲区已经满了,这时候还在往里面发送数据,则会进入阻塞状态,除非此时有其它G把缓冲区内的数据取走了,长时间阻塞则panic

通道的操作

通信共有三种操作:发送(send)、接收(receive)、关闭(close),其中发送和接收都是通过:<-符号实现的

1、发送/接收

ch := make(chan int)
ch <- 10 // 通道在前面表示,发送数据到通道ch内

注意:如果定义的通道没有缓冲区,那么则需要其它goroutine准备好读取通道的值,不然的话会出现 deadlock!异常。所以上面的程序会Panic

发送实例1、带缓冲区的发送

ch := make(chan int, 10)
ch <- 10
t := <- ch
fmt.Println(t)

打印结果

10

发送实例2、无缓存区发送,但是定义一个goroutine准备接收

ch := make(chan int)
	
go func() {
  fmt.Println(<- ch)
}()

ch <- 10

2、关闭

ch := make(chan int, 10)

close(ch)

注意:通常会在所有数据传输、接收完成关闭通道,但需要注意的是通道不是必须关闭的,它可以被垃圾回收,如果是文件的话需要手动关闭,而通道则不是必须的

关闭通道需要注意的问题:

  1. 通道关闭后,发送值会panic
  2. 通道关闭后仍然可以取值(设计主要是为了避免通道内的数据未取完),但如果通道没有值的话会获取对应类型的零值
  3. 关闭一个关闭的通道会panic
ch := make(chan int, 10)

go func() {
  fmt.Println(<-ch)
  // 通道关闭后,只能打印对应类型的零值,这里int类型会打印0
}()

close(ch)

time.Sleep(1000)

多返回值模式

当通道关闭,获取完通道的值后,再次获取也只能获取到零值,所以这时候需要进行判断,如果通道内没有值了,我们则不再进行获取

对一个通道进行接收操作时,支持如下多返回值模式

value, ok := <- ch
  • Value:从通道内获取的数据
  • ok:本次是否从通道内获取数据,true or false
ch := make(chan int)

go func() {
  v1,ok := <- ch
  fmt.Println(v1, ok)

  v2,ok := <- ch
  fmt.Println(v2, ok)
}()

ch <- 10

close(ch)

time.Sleep(3000)

打印结果:

10 true
0 false

for range获取通道值

我们可以通过for range来获取通道内的所有值,而不是通过for {}配合value,ok的方式

func main(){
  ch := make(chan int, 5)
  ch <- 1
  ch <- 2
  ch <- 3

  go f3(ch)

  time.Sleep(time.Second * 3)
}

func f3(c chan int)  {
	for t := range c{
		fmt.Println(t)
	}
}

打印结果:for range会在通道内值获取完毕后结束

1
2
3

我们可以通过len(ch)获取通过内的元素数量

range针对通道属于阻塞式取值,如果没有数据的话就会一直阻塞获取,长时间阻塞的话会被go语言判断成死锁(deadlock)从而panic

只有close掉了通道range才会结束,但是上面并没有close通道,能够正常运行的原因是因为在goroutine内执行的,实际上它已经产生阻塞了,但是由于我们主程序结束了,所以goroutine内的阻塞我们并没有感知到,如下代码示例:

package main

import (
	"fmt"
	"time"
)

func main(){
	ch := make(chan int, 5)
	ch <- 1
	ch <- 2
	ch <- 3

	go f3(ch)

	time.Sleep(time.Second * 3)
}

func f3(c chan int)  {
	for t := range c{
		fmt.Println(t)
	}

	fmt.Println(1111)
}

上面程序打印:

1
2
3

并没有打印1111,这是因为range读取通道的时候已经阻塞了,但是在主程序sleep时间过后整个程序结束了,所以我们没有感觉到for range读取通道数据这种方式有什么问题。

如果想要for range能够正常读取通道数据,我们需要在确认通道数据发送完毕后close

package main

import (
	"fmt"
	"time"
)

func main(){
	ch := make(chan int, 5)
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch) // 关闭通道


	go f3(ch)

	time.Sleep(time.Second * 3)
}

func f3(c chan int)  {
	for t := range c{
		fmt.Println(t)
	}

	fmt.Println(1111)
}

打印结果:

1
2
3
1111

单向通道

一种要么只能从里面读取数据,要么只能往里面写入数据的通道

通常我们会将通道作为参数在函数之间传递,一般情况需要限制通道在不同地方的使用,比如:某些函数只能进行读取数据的操作,或者发送数据的操作。

比较景经典的就是生产者与消费者模型:生产者(Producer)往通道里面写入数据、消费者(Consumer)从通道内读取数据消费。Consumer是通过Producer返回的单向通道进行数据读取。
在其它函数,例如Python内,线程间通信可以通过Queue实现

Go语言层面提供单向通道来限制通道的使用场景

  • <- chan int 只能读取数据的通道,不能发送数据
  • chan <- int 只能写入数据的通道,不能读取数据

我们定义一个Producer用于返回一个单向通道

// Producer 函数返回了一个只能读取数据的单向通道
func Producer() <-chan int {
	ch := make(chan int, 10)

	go func() {
		for i := 0; i < 10; i++ {
			if i%2 == 1 {
				ch <- i
			}
		}
		close(ch)
    // 数据写入完成后关闭通道;虽然关闭掉通道任然可以实现类似单向通道的效果,但是并没有从代码层面去限制通道是否属于单向通道,只会再使用的时候察觉,这种写法并不规范
	}()
  	// 这里关闭通道是为了for range能够从通道内取完数据后不阻塞


	// 上面创建了一个goroutine去往通道内写入值,返回该函数是先返回通道,然后goroutine往里面写入值(goroutine的执行需要时间,不影响后面代码继续执行)

	return ch
}

定义一个Comsumer从只读通道内获取数据

// Consumer 消费者,从通过内获取数据计算并返回结果并
func Consumer(ch <-chan int) int {

	sum := 0
	for {
		sum += <- ch
	}

	return sum
}
func main() {
  // 定义通道类型的变量名最好见名知义(起码知道它是一个通道类型)
	vCh := Producer()

	fmt.Println(Consumer(vCh))
}

执行结果:

1
3
5
7
9 

不同通道状态的对应的操作结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o2DycDtK-1689153452030)(/Users/apple/Library/Application Support/typora-user-images/image-20230530203151606.png)]

注意:对已经关闭的通道close会panic

select

select类似于switch方法,它也有一系列的case分支和default分之。不同的是,select里面的case是通道的通信(接收或者发送)过程。直到某个case的通信操作完成后,才会执行对应case分支的语句

select语句具备以下特点

  1. 可以处理一个或多个channel的发送/接收操作
  2. 如果多个case满足,会随机执行一个case的分支语句(真的随机)
  3. 对于没有case的select会一直阻塞,可用于阻塞main函数,防止退出(目的是为了让程序内的其它goroutine持续运行)
ch1 := make(chan int, 10)
ch2 := make(chan int, 10)

ch1 <- 10
ch1 <- 20

select {
case data := <-ch1:
  // 从ch1取值成功后执行
  fmt.Println("第一个case,ch1的数据", data)
case data := <-ch1:
  // 从ch1取值成功后执行
  fmt.Println("第二个case,ch1的数据", data)
case ch2 <- 10:
  // 往ch2发送值成功后执行
  fmt.Println("ch2发送了数据")
}

上面属于3个case都满足的情况,那么会随机执行一个case的代码块

1、猜测下面程序打印结果

ch := make(chan int, 1)
for i := 1; i < 10; i++ {
		select {
		case data := <-ch:
			fmt.Println(data)
		case ch <- i:
		}
	}

程序解析

注意观察ch缓冲区大小,第一次i等于1的时候,缓冲区没有值,所以第一个case不行执行,第二个case执行了,往里面存放了数据1
第二次i=2,如果缓冲区再大一些,那么两个case都能满足则会随机执行一个,但是由于缓冲区已经满了,所以第二个case执行不了,那么则执行第一个case打印:1
第三次i=3:重复第一次的行为,缓冲区无数据,执行第二个case
第四次i=4:重复第二次的行为,缓冲区有无数据,无法执行第二个case,所以执行第一个读取

所以打印结果是:1、3、5、7

2、猜测下面程序打印结果

ch2 := make(chan string)

go func() {
  time.Sleep(3 * time.Second)
  ch2 <- "123"
}()

select {
case result := <-ch2:
  fmt.Println(result)
case <-time.After(time.Second):
  return
}

程序解析:

解析:上面第一个case是接收ch2通道的数据,但是由于ch2通道在一个goroutine里面,3s后才会写入数据,而第二个case属于一个定时时间,我们设置的定时时间为1s,所以,第二个会先执行,第一个case只有通道写入数据才会执行。
上面程序不会打印任何结果就结束了,并且会操作goroutine内存泄露,因为select执行完后,goroutine内写入到通道的数据由于没有接收,并且没有定义缓冲区,所以会一直阻塞导致goroutine无法结束(如果我们这个程序一直属于运行状态,当前main函数测试不会出现这种情况)

select也可以用于定义超时时间,我们可以设置在一定时间内没有获取到通道内的值,则退出

goroutine、channel案例

使用 goroutine和channel 实现一个计算int64随机数各位数和的程序,例如生成随机数61345,计算其每个位数上的数字之和为19。

  1. 开启一个 goroutine 循环生成int64类型的随机数,发送到jogChan
  2. 开启24个 goroutine 从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
  3. 主 goroutine 从resultChan取出结果并打印到终端输出

程序实现

// 随机数生成,写入通道
func generateNumber(c chan<- int64) {

	for {
		rand.Seed(time.Now().UnixNano())

		select {
		case c <- rand.Int63n(1000000):
		default:
			time.Sleep(time.Microsecond * 100)

		}
	}
}

func resultCount(jogChan <-chan int64, resultChan chan<- int64) {
	// 开启24个goroutine
	for i := 0; i < 24; i++ {
		go countNum19(jogChan, resultChan)
	}
}

// 计算每个单数相加之和为19发送到通道
func countNum19(jogChan <-chan int64, resultChan chan<- int64) {
	for {
		select {
		case num := <-jogChan:
			if res := sum(num); res > 0 {
				resultChan <- res
			}
		default:
			time.Sleep(time.Microsecond)
		}
	}
}

func sum(num int64) int64 {
	var res int64

	result := num
	
  // 计算每个单数的结果
	for result > 0 {
		res += result % 10
		result /= 10
	}
  
	if res == 19 {
		fmt.Println(num)
		return num
	}

	return 0
}

执行结果:每个数字的个数加起来==19

2166031
3202057
4105045
1041715
510094
2060164
2504413
370108
3124360
1511515
3324043
332236
5331106
9310105
4813201
3220093
4202902
2421703
2130382
5007160
3074221
1022338
1321714
1373005
1413730
2138113
6371200
5114035

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

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

相关文章

C++函数cincout的基本用法

C的输入输出格式可比C语言的简单得多&#xff0c;输入函数是cin()&#xff0c;输出函数是cout。 首先是cin的基本用法如下 (假设n 5) #include <iostream> using namespace std; int main() {int n;cin>>n;return 0; } 而它的结果则是 cin不仅可以输入int类型…

从单目数据生成3D模型,Meta智能眼镜摄像头校正算法揭秘

众所周知&#xff0c;Meta下一个目标就是AR眼镜&#xff0c;尽管开发AR比VR面临更多复杂的难题&#xff0c;因此可能还要过一段时间才能看到Meta的AR眼镜。目前&#xff0c;该公司已推出了第一代Ray-Ban Stories智能眼镜&#xff0c;特点是搭载的双摄像头不仅可以拍照&#xff…

Verilog基础之十五、锁存器实现

目录 一、前言 二、工程设计 2.1 工程代码 2.2 综合结果 2.3 Latch实现 一、前言 在之前的文章中介绍过寄存器&#xff0c;本节介绍一个类似的逻辑单元&#xff1a;锁存器。在大部分的资料和文章介绍中&#xff0c;都是告诉读者设计中应尽量避免出现锁存器&#xff0c;这主…

从0到1,带你深入了解react fiber

react16之后&#xff0c;react引入了fiber架构&#xff0c;那么它究竟是什么&#xff0c;如何实现的呢&#xff1f;下面就让笔者带你掰扯掰扯&#xff0c;如有错误&#xff0c;欢迎指正 目录 渲染过程 react15 react16 为什么要引入fiber 不可中断原因 fiber详解 是什么…

百度墨斗鱼文库创作中心源码分析

前言 公司解散&#xff0c;待业中&#xff0c;耗时一天研究了一下百度墨斗鱼文库创作中心源码。实现了后台自动完成任务并通知。 下面主要分析一下实现思路和难点 一&#xff0c;实现思路 调用接口查询未回答的题目列表 合并多个tab下的题目 设置黑白名单&#xff0c;这里…

你知道为什么不用XFP光模块了吗?

在光纤通信应用领域中&#xff0c;10G光模块凭借着较低的成本和功耗被广泛应用于学校、企业等应用场景中。XFP和SFP是10G光模块常见的两种封装类型&#xff0c;那为什么现在市场上XFP光模块应用比较少了呢&#xff1f;下面我们来简单分析一下原因。 一、XFP与SFP光模块的概述 …

从小白到大神之路之学习运维第58天--------Firewalld防火墙

第三阶段基础 时 间&#xff1a;2023年7月12日 参加人&#xff1a;全班人员 内 容&#xff1a; Firewalld防火墙 目录 Firewalld防火墙 一、防火墙 1、netfilter和防火墙管理工具 2、防火墙配置模式 3、Firewalld数据流处理的方式 4、firewalld区域类型 1&#x…

【SVN wc.db 删除不掉的问题】

SVN wc.db 删除不掉的问题 方案1&#xff1a;任务管理器 >性能 >打开资源监视器 > CPU >搜索句柄&#xff0c;关闭相关线程&#xff0c;重试删除。若删除explorer.exe导致资源管理器不显示&#xff0c;在任务管理器新建该任务即可“explorer.exe” 方案2&#xff1…

2023-07-12:RocketMQ如何做到消息不丢失?

2023-07-12&#xff1a;RocketMQ如何做到消息不丢失&#xff1f; 答案2023-07-12&#xff1a; RocketMQ通过刷盘机制、消息拉取机制和ACK机制等多种方式来确保消息投递的可靠性&#xff0c;防止消息丢失。 1.刷盘机制 RocketMQ中的消息分为内存消息和磁盘消息&#xff0c;内…

Acwing.858 Prim算法求最小生成树(朴素Prims算法)

题目 给定一个n个点m条边的无向图&#xff0c;图中可能存在重边和自环&#xff0c;边权可能为负数。求最小生成树的树边权重之和&#xff0c;如果最小生成树不存在则输出impossible。 给定一张边带权的无向图G(V,E)&#xff0c;其中V表示图中点的集合&#xff0c;E表示图中边的…

赛效:如何用在线压缩GIF图片

1&#xff1a;在电脑网页上打开并登录快改图&#xff0c;点击左侧菜单栏里的“GIF压缩”。 2&#xff1a;点击页面中间的上传按钮&#xff0c;将电脑本地的GIF文件上传上去。 3&#xff1a;GIF文件上传成功后&#xff0c;设置下方压缩设置&#xff0c;点击右下角“开始压缩”。…

APP外包开发硬件通讯协议

开发APP时会遇到需要与硬件设备通讯的业务场景&#xff0c;常见的硬件设备有健康设备(手环、血压计、血糖仪等)、智能家居设备(冰箱、灯、电视等)、工业设备等等&#xff0c;这些设备的通讯要求各不相同&#xff0c;因此通讯协议也不相同。今天和大家分享这方面的知识&#xff…

buu-Reverse-[2019红帽杯]childRE

目录 [2019红帽杯]childRE 修饰函数名和函数签名是什么&#xff1f; 对于变换部分的具体分析&#xff1a; [2019红帽杯]childRE 下载附件&#xff0c;查壳&#xff0c;无壳 在IDA中打开&#xff0c;定位主函数 int __cdecl main(int argc, const char **argv, const char …

深度学习笔记之Transformer(八)Transformer模型架构基本介绍

机器学习笔记之Transformer——Transformer模型架构基本介绍 引言回顾&#xff1a;简单理解&#xff1a; Seq2seq \text{Seq2seq} Seq2seq模型架构与自编码器自注意力机制 Transformer \text{Transformer} Transformer架构关于架构的简单认识多头注意力机制包含掩码的多头注意力…

kubernetes 1.27.3 集群部署方案

一、准备环境 1.1 Kubernetes 1.27.3 版本集群部署环境准备 1.1.1 主机硬件配置说明 cpu内存硬盘角色主机名系统版本 8C 8G 1024GB master master01 centos 7.9 8C 16G 1024GB worker(node) worker01 centos 7.9 8C 16G 1024GB worker(node) worker…

EasyCVR平台Ehome协议接入,设备管理中出现新增通道按钮的问题优化

EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等&#xff0c;能对外分发RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。 有用…

软件测试重要性

导言&#xff1a; 在现代软件开发中&#xff0c;软件测试扮演着确保质量的重要角色。通过对软件系统的验证和验证&#xff0c;软件测试有助于发现潜在的缺陷和问题&#xff0c;并确保软件在部署之前能够达到预期的质量标准。本文将深入探讨软件测试的重要性、测试的类型以及成功…

docker常用功能以及mysql实际使用【推荐】

docker常用功能以及mysql实际使用&#xff1a; 一、docker常用命令&#xff1a; 查看版本 docker -v [rootlocalhost ~]# docker -v Docker version 23.0.4, build f480fb1 [rootlocalhost ~]# 2. 查看 Docker 中已存在的镜像 docker images [rootlocalhost ~]# docker ima…

从零开始制作一个Web蜜罐扫描器(3)

从零开始制作一个Web蜜罐扫描器(2)_luozhonghua2000的博客-CSDN博客 那么经过字典的优化,最终就得到了一份ok的字典,如下所示 基本上都是有效的api,那么字典清洗这一步到这里就算完成了。 有了一份好的字典,准确性的问题就迎刃而解。 。 怎么快速且准确且批量的找到蜜储 这…

jenkins实现easyswoole 持续集成/持续部署

jenkins环境jenkins需要使用root用户启动可通过修改 vim /etc/sysconfig/jenkins改为root,也可直接命令行root启动新增流水线项目安装远程构建插件Generic Webhook Trigger勾选触发远程构建保存之后,访问 /generic-webhook-trigger/invoke?tokeneasyswoole-test,即可自动bui…