Go基础学习08-并发安全型类型-通道(chan)深入研究

文章目录

  • chan基础使用和理解
  • 通道模型:单通道、双通道
    • 双向通道
    • 单向通道
      • 单向通道的作用
  • 缓冲通道和非缓冲通道数据发送和接收过程
    • 缓冲通道
    • 非缓冲通道
  • 通道基本特性
  • 通道何时触发panic
  • Channel和Select结合使用
    • Select语句和通道的关系
    • Select语句的分支选择规则有那些
    • Select和Channel结合使用案例一
    • Select和Channel结合使用案例二
  • Select和for联合使用时如何退出最外层循环

在前面学习中了解到对于单值变量,如:int、string;多值变量,如:map存在多协程对资源竞争的并发问题,为了解决并发性通常需要引入sync.Mutex解决。 通道类型本身就是并发安全的,是Go语言自带的、唯一一个可以满足并发安全性的类型。

chan基础使用和理解

package main

import (
	"fmt"
)

func main() {
	// 使用make声明并初始化一个int型的带缓冲的通道,并将其容量设置为3
	ch1 := make(chan int, 3)
	// 使用make声明并初始化一个int型的不带缓冲的通道,其容量为0
	ch2 := make(chan int)
	// 声明一个int型的通道
	var ch3 chan int
	// 使用接送操作符 <- 向通道ch1中发送int型数据1
	go func(){
		ch1 <- 1
		ch1 <- 2
		ch1 <- 3
	}()
	// 使用接送操作符 <- 从通道ch1中读取数据,下面的短变量表达式左边有两个变量num, ok:其中ok用于判断通道ch1是否关闭,
	// 当ok == ture时表示通道没有关闭,可以读取数据并将其保存到变量nums中,当ok == false时表示通道关闭,不能读取数据。
	// 此外读取通道中的数据,可以直接使用num := <- ch1进行读取,不添加第二个判断通道是否关闭的条件,此时有风险存在
	num, ok := <-ch1
	if ok {
		fmt.Println(num)
	}
	close(ch1)
	close(ch2)
	// close(ch3)
}

对于通道的基本声明方式有三种:声明并初始化带缓冲的通道(ch1);声明并初始化一个不带缓冲的通道(ch2);仅仅声明一个通道(ch3)
什么是通道:一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。

上述代码:对于通道3由于仅仅声明没有进行初始化,所以不能执行关闭操作。 对于未初始化的通道执行close操作报panic:

panic: close of nil channel
// go语言中通道类型为引用类型,故只声明而未初始化时通道值为nil。

通道模型:单通道、双通道

双向通道

channel两端的goroutine既可以发送数据也可以接受数据。
在这里插入图片描述
默认情况下使用创建的通道即为双向通道。

单向通道

创建的channel规定了数据流向,对于channel双端,分别一端作为发送者(producer)、一端作为接收者(consumer),只能发送或者接收。
在这里插入图片描述
创建单向通道:对于单向通道而言,发送和接收均是站在数据的角度:

  • 如果向通道中发送数据,则为发送通道。
  • 如果从通道中接收数据,则为接收通道。
// 单向发送通道
 ch1 := make(chan<-, 2)
 var ch2 chan<-
 // 单向接收通道
 ch3 := make(<-chan, 2)
 var ch4 <-chan

单向通道的作用

  • 单向通道的主要约束其他代码的行为。 看下面示例代码:
package main

import "fmt"

/*
*
创建单向通道
*/
func main() {
	// 定义带缓冲通道
	ch := make(chan int, 3)
	producer(ch, 7)
	ans := consumer(ch)
	fmt.Printf("main function get num is %v\n", ans)
}
func producer(ch chan<- int, num int) {
	ch <- num
}
func consumer(ch <-chan int) (ans int) {
	ans, ok := <-ch
	if ok {
		fmt.Printf("consumer get data is %v\n", ans)
		return ans
	}
	return 0
}

在Go语言中把元素类型匹配的双向通道传递给单向通道,会自动把双向通道转换为函数需要的单向通道(发送 or 接收)

上述代码简单定义一个生产者函数,向通道中写入数据;定义一个消费者函数,从特定通道中消费数据。借助于通道实现了消息的特定方向移动。
更普适应的使用——对接口的行为进行约束:从而对接口的所有实现者都进行约束的目的。

type Notifier interface{
	SendInt(ch chan<- int)
}

上述代码中我们对Notidier接口的SendInt函数使用了单向通道约束,在所有Notifier接口的所有实现类型中,SendInt函数都会受到单向通道的约束。

  • 在函数声明的结果列表中使用单向通道:
package main

import "fmt"

func main() {
	getChan := getIntChan()
	for elem := range getChan {
		fmt.Println(elem)
	}
}

// 获取一个单向发送通道
func getIntChan() <-chan int {
	num := 5
	ch := make(chan int, num)
	for i := 0; i < num; i++ {
		ch <- i
	}
	close(ch)
	return ch
}

调用getIntChan()函数获取一个单向接收通道getChan,随后使用带有range的for遍历接收通道中的每一个元素并对其打印。
结合通道的特性:上述代码在getChan没有元素或者,为nil时会阻塞在for的那行代码处,上述只是展示单向通道的用于,不具有实际意义。

缓冲通道和非缓冲通道数据发送和接收过程

缓冲通道

	ch := make(chan int, 5)

上述代码创建一个容量为5的双向通道。在Go语言中对于通道的初始化不像切片初始化可以指定切片的长度,在通道初始化时只需要设置一个缓冲容量,通道中的长度含义就是通道中的元素个数。
带缓冲通道数据发送:元素进行复制,将副本放入通道中,同时将通道中的原值删除。

如果缓冲容量已经满了,此时新来的goroutine会被放到一个FIFO队列中,等到缓冲区有容量,此时最前面的goroutine执行元素放置操作。

带缓冲通道数据接收:生成在通道中的元素值的副本,将副本给到接收方。

接收操作类似,如果缓冲区中没有元素,此时所有接收者会阻塞,并进入一个FIFO队列中,当缓冲区有数据后,在队列最前面的goroutine会获取channel最前端的元素值。

缓冲通道何时阻塞:

  • 缓冲区满时,所有发送goroutine阻塞。
  • 缓冲区没有数据时,所有接收goroutine阻塞。
  • 对与nil的通道,无论何时一直阻塞。

在一般情况下缓冲通道起到数据传输的中间件作用,需要将数据暂时存储到缓冲区中,但是当缓冲通道执行发送操作时发现空的通道中正好有等待的接收者,此时发送者会直接将数据拷贝给接收者,减少数据在缓冲区的临时拷贝。(类似与非缓冲通道)

非缓冲通道

	ch := make(chan int)	// 创建非缓冲通道

非缓冲通道必须等待发送方和接收方均就绪,才会进行数据发送以及接收,否则对应的goroutine处于组塞。并且数据发送以及接收过程,在通道中不会产生数据副本,发送方数据拷贝后直接通过通道传递给接收方。

非缓冲通道何时阻塞:

  • 发送方或接收方任何一方没有准备好,都会阻塞。
  • 对于nil的通道,无论何时一直阻塞。

通道基本特性

  • 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的:在同一时刻,Go 语言的运行时系统只会执行对同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。类似的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操作中的某一个。直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。
  • 发送操作和接收操作中对元素值的处理都是不可分割的。:保证通道中元素值的完整性,同时保证通道操作的唯一性
    • 发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。
    • 接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。
  • 发送操作在完全完成之前会被阻塞。接收操作也是如此。

通道何时触发panic

  • 对于值为nil(未初始化)的通道执行close()操作。
  • 对于已经执行了close()操作的通道再次执行close()操作。
  • 对于执行了close()操作的通道执行收发操作。

Channel和Select结合使用

Select语句和通道的关系

  1. select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。
  2. select语句的分支分为两种,一种叫做候选分支,另一种叫做默认分支。候选分支总是以关键字case开头,后跟一个case表达式和一个冒号,然后我们可以从下一行开始写入当分支被选中时需要执行的语句。
  3. 默认分支其实就是 default case,因为,当且仅当没有候选分支被选中时它才会被执行,所以它以关键字default开头并直接后跟一个冒号。同样的,我们可以在default:的下一行写入要执行的语句。
  4. 由于select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式,比如接收表达式。

Select语句的分支选择规则有那些

  1. 对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式,同时也可能会包含其他的表达式。比如,如果case表达式是包含了接收表达式的短变量声明时,那么在赋值符号左边的就可以是一个或两个表达式,不过此处的表达式的结果必须是可以被赋值的。当这样的case表达式被求值时,它包含的多个表达式总会以从左到右的顺序被求值。

  2. select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。结合上一条规则,在select语句开始执行时,排在最上边的候选分支中最左边的表达式会最先被求值,然后是它右边的表达式。仅当最上边的候选分支中的所有表达式都被求值完毕后,从上边数第二个候选分支中的表达式才会被求值,顺序同样是从左到右,然后是第三个候选分支、第四个候选分支,以此类推。

  3. 对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。在这种情况下,我们可以说,这个case表达式所在的候选分支是不满足选择条件的。

  4. 仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select语句(或者说它所在的 goroutine)就会被唤醒,这个候选分支就会被执行。

  5. 如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。注意,即使select语句是在被唤醒时发现的这种情况,也会这样做。

  6. 一条select语句中只能够有一个默认分支。并且,默认分支只在无候选分支可选时才会被执行,这与它的编写位置无关。

  7. select语句的每次执行,包括case表达式求值和分支选择,都是独立的。不过,至于它的执行是否是并发安全的,就要看其中的case表达式以及分支中,是否包含并发不安全的代码了。

Select和Channel结合使用案例一

package main

import (
	"fmt"
	"math/rand"
)

/*
*
channel和select的联合使用
*/
func main() {
	// 创建一个多维通道
	chs := []chan int{
		make(chan int, 1),
		make(chan int, 1),
		make(chan int, 1),
	}
	// 创建随机数
	index := rand.Intn(3)
	fmt.Printf("index is: %v\n", index)
	chs[index] <- 1
	select {
	case elem := <-chs[0]:
		fmt.Printf("通道0被选中,元素为:%v\n", elem)
	case elem := <-chs[1]:

		fmt.Printf("通道1被选中,元素为:%v\n", elem)
	case elem := <-chs[2]:
		fmt.Printf("通道2被选中,元素为:%v\n", elem)
	default:
		fmt.Println("error.")
	}
}
  1. 如果像上述示例那样加入了默认分支,那么无论涉及通道操作的表达式是否有阻塞,select语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选中并执行。
  2. 如果没有加入默认分支,那么一旦所有的case表达式都没有满足求值条件,那么select语句就会被阻塞。直到至少有一个case表达式满足条件为止。
  3. 可能会因为通道关闭了,而直接从通道接收到一个其元素类型的零值。所以,在很多时候,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。
  4. select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句

Select和Channel结合使用案例二

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 1)
	time.AfterFunc(time.Second*2, func() {
		close(ch)
	})
	select {
	case _, ok := <-ch:
		if !ok {
			fmt.Println("The candidate case is closed.")
			break
		}
		fmt.Println("The candidate case is selected.")
	}
}

声明并初始化了一个叫做intChan的通道,然后通过time包中的AfterFunc函数约定在二秒钟之后关闭该通道。后面的select语句只有一个候选分支,我在其中利用接收表达式的第二个结果值对intChan通道是否已关闭做了判断,并在得到肯定结果后,通过break语句立即结束当前select语句的执行。

Select和for联合使用时如何退出最外层循环

  • break和标签配合使用,直接break处出指定的循环体。
  • goto语句跳转到指定标签。

代码示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		ch := make(chan int, 3)
		ch <- 1
		ch <- 2
		ch <- 3
		time.AfterFunc(time.Second*3, func() {
			close(ch)
		})
		// 方式一:使用break配合标签实现
	loop:
		for {
			select {
			case _, ok := <-ch:
				if !ok {
					fmt.Println("The ch is closed.")
					break loop // 使用return配合标签退出for循环
				}
				fmt.Println("The ch case is selected.")
			}
		}
		fmt.Println("The end of ch.")
	}()

	go func() {
		ch1 := make(chan int, 3)
		ch1 <- 5
		ch1 <- 6
		ch1 <- 7
		time.AfterFunc(3*time.Second, func() {
			close(ch1)
		})

		for {
			select {
			case _, ok := <-ch1:
				if !ok {
					fmt.Println("The ch1 is closed.")
					goto loop1 // 使用goto配合标签实现退出select和for的结合使用
				}
				fmt.Println("The ch1 case is selected.")
			}
		}
	loop1:
		fmt.Println("The end of ch1.")
	}()
	// 等待10防止主协程退出后所有子协程死亡
	time.Sleep(time.Second * 10)
	fmt.Println("The end of main.")
}

上述代码执行结果:

The ch1 case is selected.
The ch1 case is selected.
The ch1 case is selected.
The ch case is selected.
The ch case is selected.
The ch case is selected.
The ch is closed.
The end of ch.
The ch1 is closed.
The end of ch1.
The end of main.

上述代码创建两个协程,一个协程中验证break配合标签实现退出外层for循环,另一个协程中验证goto配合标签实现退出外层for循环。同时为了防止main协程退出导致两个子协程退出,在主协程中调用time.Sleep()函数休眠10秒钟。

如果在select语句中发现某个通道已关闭:为了防止再次进入这个分支,可以把这个channel重新赋值成为一个长度为0的非缓冲通道,这样这个case就一直被阻塞了

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

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

相关文章

基于Hive和Hadoop的招聘分析系统

本项目是一个基于大数据技术的招聘分析系统&#xff0c;旨在为用户提供全面的招聘信息和深入的职位市场分析。系统采用 Hadoop 平台进行大规模数据存储和处理&#xff0c;利用 MapReduce 进行数据分析和处理&#xff0c;通过 Sqoop 实现数据的导入导出&#xff0c;以 Spark 为核…

旺店通ERP集成金蝶K3(旺店通主供应链)

源系统成集云目标系统 金蝶K3介绍 金蝶K3是一款ERP软件&#xff0c;它集成了供应链管理、财务管理、人力资源管理、客户关系管理、办公自动化、商业分析、移动商务、集成接口及行业插件等业务管理组件。以成本管理为目标&#xff0c;计划与流程控制为主线&#xff0c;通过对成…

Vue开发前端图片上传给java后端

前端效果图 图片上传演示 1 前端代码 <template><div><!-- 页面标题 --><h1 class"page-title">图片上传演示</h1><div class"upload-container"><!-- 使用 van-uploader 组件进行文件上传&#xff0c;v-model 绑…

Golang | Leetcode Golang题解之第443题压缩字符串

题目&#xff1a; 题解&#xff1a; func compress(chars []byte) int {write, left : 0, 0for read, ch : range chars {if read len(chars)-1 || ch ! chars[read1] {chars[write] chwritenum : read - left 1if num > 1 {anchor : writefor ; num > 0; num / 10 {…

BFS的上下左右搜索问题(递归和迭代)

目录 一题目&#xff08;单词搜索问题&#xff09;&#xff1a; 二思路解释&#xff1a; 三解答代码&#xff1a; ​编辑 四题目&#xff08;腐烂的苹果&#xff09;&#xff1a; 五思路解释&#xff1a; 六解答代码&#xff1a; ​​​​ 一题目&#xff08;单词…

生信初学者教程(十五):差异结果的热图

文章目录 介绍加载R包导入数据画图函数热图输出结果总结介绍 热图是一种数据可视化工具,用于展示矩阵数据中的数值大小,通过颜色的深浅或色调变化来表示不同的数值。通常用于生物信息学、统计学和数据分析等领域,以便直观地比较和分析大量数据。 热图展示的是一个二维的数据…

ESP32 Bluedroid 篇(1)—— ibeacon 广播

前言 前面我们已经了解了 ESP32 的 BLE 整体架构&#xff0c;现在我们开始实际学习一下Bluedroid 从机篇的广播和扫描。本文将会以 ble_ibeacon demo 为例子进行讲解&#xff0c;需要注意的一点是。ibeacon 分为两个部分&#xff0c;一个是作为广播者&#xff0c;一个是作为观…

“数字武当”项目荣获2024年“数据要素×”大赛湖北分赛文化旅游赛道一等奖

9月26日&#xff0c;由国家数据局、湖北省人民政府指导的首届湖北省数据要素创新大会暨2024年“数据要素”大赛湖北分赛颁奖仪式在湖北武汉举行。由大势智慧联合武当山文化旅游发展集团有限公司参报的武当山“数字武当”项目&#xff0c;荣获文化旅游赛道一等奖。 据悉&#x…

VIVADO IP核之FIR抽取器多相滤波仿真

VIVADO IP核之FIR抽取器多相滤波仿真&#xff08;含有与MATLAB仿真数据的对比&#xff09; 目录 前言 一、滤波器系数生成 二、用MATLAB生成仿真数据 三、VIVADO FIR抽取多相滤波器使用 四、VIVADO FIR抽取多相滤波器仿真 五、VIVADO工程下载 总结 前言 关于FIR低通滤波…

[论文精读]TorWard: Discovery, Blocking, and Traceback of Malicious Traffic Over Tor

期刊名称&#xff1a;IEEE Transactions on Information Forensics and Security 发布链接&#xff1a;TorWard: Discovery, Blocking, and Traceback of Malicious Traffic Over Tor | IEEE Journals & Magazine | IEEE Xplore 中文译名&#xff1a;TorWard&#xff1a;…

基于python+django+vue的电影数据分析及可视化系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

【工具分享】BigBobRoss勒索病毒解密工具

前言 BigBobRoss勒索软件首次被发现于2019年初。它由C编写&#xff0c;并使用了QT框架。该勒索软件的加密方法相对基础&#xff0c;使用AES-128 ECB算法对受害者的文件进行加密。尽管加密技术并不复杂&#xff0c;但它的传播和影响力迅速扩展&#xff0c;导致大量用户的数据被…

vs2019从一个含main函数的cpp文件到生成动态生成库

小白&#xff0c;只会写简单的cpp文件&#xff0c;算法写完之后需要项目工程化&#xff0c;和上位机开发人员完成交接&#xff0c;记录一下。 文章目录 一、VS创建空项目二、编写代码 一、VS创建空项目 点击下一步&#xff0c; 我这里创建的项目名称为LidarCoreDetection 位置D…

AdaptIoT——制造业中使用因果关系的自我标签系统

0.概述 论文地址&#xff1a;https://arxiv.org/abs/2404.05976 在许多制造应用中&#xff0c;机器学习&#xff08;ML&#xff09;已被证明可以提高生产率。针对制造业应用提出了一些软件和工业物联网&#xff08;IIoT&#xff09;系统&#xff0c;以接收这些 ML 应用。最近&…

FastAPI 第六课 -- 请求和响应

目录 一. 前言 二. 请求数据 2.1. 查询参数 2.2. 路径参数 2.3. 请求体 三. 响应数据 3.1. 返回 JSON 数据 3.2. 返回 Pydantic 模型 3.3. 请求头和 Cookie 四. 重定向和状态码 五. 自定义响应头 一. 前言 在 FastAPI 中&#xff0c;请求&#xff08;Request&#…

每日论文7-17MWCL基于IMOS的小vco增益变化的VCO

《Small VCO-Gain Variation Adding a Bias-Shifted Inversion-Mode MOS Varactor》17MWCL 对于PLL来说&#xff0c;其中VCO的调谐增益KVCO越线性&#xff0c;其变化程度ΔKvco越小&#xff0c;对PLL的稳定有较大的好处。这篇文章给了一个很简单朴素而有效的补偿var非线性的方…

nuclei配合burpsuite快速生成POC

nuclei配合burpsuite快速生成POC 简介 Nuclei是一款基于YAML语法模板的开发的定制化快速漏洞扫描器。它使用Go语言开发&#xff0c;具有很强的可配置性、可扩展性和易用性 官网&#xff1a;https://nuclei.projectdiscovery.io Nuclei项目地址&#xff1a;https://github.com/…

2024热门AIPPT工具大盘点

随着人工智能技术的飞速发展&#xff0c;一种全新的 PPT 制作方式应运而生——Ai 制作 PPT。它如同一位智能助手&#xff0c;为我们带来了高效、创新且个性化的 PPT 制作体验。今天我们一起探讨有哪些工具可以助力我们轻松打造出令人惊艳的演示文稿的。 1.笔灵AIPPT 链接一下…

从零开始手写STL库:Stack

从零开始手写STL库–Stack的实现 Gihub链接&#xff1a;miniSTL 文章目录 从零开始手写STL库–Stack的实现一、stack是什么&#xff1f;二、stack要包含什么函数总结 一、stack是什么&#xff1f; 栈是一种后进先出&#xff08;LIFO&#xff0c;Last In First Out&#xff09…

【STM32】江科大STM32笔记汇总(已完结)

STM32江科大笔记汇总 STM32学习笔记课程简介(01)STM32简介(02)软件安装(03)新建工程(04)GPIO输出(05)LED闪烁& LED流水灯& 蜂鸣器(06)GPIO输入(07)按键控制LED 光敏传感器控制蜂鸣器(08)OLED调试工具(09)OLED显示屏(10)EXTI外部中断(11)对射式红外传感器计次 旋转编码器…