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

news2024/9/28 22:24:43

文章目录

  • 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的那行代码处,上述只是展示单向通道的用于,不具有实际意义。**
# 缓冲通道和非缓冲通道数据发送和接收过程
## 缓冲通道
```go
	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语句跳转到指定标签。
// break退出指定循环
ch1 := make(chan int, 1)
time.AfterFunc(time.Second, func() { close(ch1) })
loop:
for {
select {
case _, ok := <-ch1:
if !ok {
break loop
}
fmt.Println("ch1")
}
}
fmt.Println("END")

// goto配合标签:
ch1 := make(chan int, 1)
time.AfterFunc(time.Second, func() { close(ch1) })
for {
select {
case _, ok := <-ch1:
if !ok {
goto loop
}
fmt.Println("ch1")
}
}
loop:
fmt.Println("END")

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

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

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

相关文章

Java底层并发:线程、volatile

在Java的并发编程中&#xff0c;线程、volatile关键字、原子性、临界区以及DelayQueue是一些重要概念。理解这些内容对于编写高效且线程安全的程序至关重要。 1. 线程的基本概念 Java中的线程是程序执行的最小单位。Java提供了多种创建线程的方式&#xff0c;最常用的方式是继…

英特尔终于找到了Raptor Lake处理器崩溃与不稳定问题的根源

技术背景 在过去的几个月里&#xff0c;一些用户报告称他们的第13代和第14代Intel Core“Raptor Lake”处理器遇到了系统崩溃和不稳定的情况。这些问题最初在2024年7月底被英特尔识别出来&#xff0c;并且初步的诊断显示&#xff0c;这些问题与微码有关&#xff0c;该微码使CP…

【JavaEE】——各种“锁”大总结

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯&#xff0c; 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01;希望本文内容能够帮助到你&#xff01; 目录 一&#xff1a;乐观锁和悲观锁 1&#xff1a;乐观锁 2&#xff1a;悲观锁 3&#xff1a;总结 二&am…

人工智能实战用折线图解读产业GDP发展态势

内容提要 项目分析项目实战 一、项目分析 1、问题提出 我们拿到一大堆关于GDP的数据&#xff0c;如何从这些表面看起来杂乱无章的数据中解读出一些有价值的信息呢? 显然&#xff0c;如果能将这些数据以图形的方式展现出来&#xff0c;例如将这些数据值随时间&#xff08;…

备考中考的制胜法宝 —— 全国历年中考真题试卷大全

在中考这场重要的战役中&#xff0c;每一分都至关重要。为了帮助广大考生更好地备考&#xff0c;我们精心整理了这份全国历年中考真题试卷大全&#xff0c;旨在为大家提供最全面、最权威的备考资料。 文章目录 1. 全科覆盖&#xff0c;无遗漏2. 历年真题&#xff0c;权威可靠3.…

【微服务】springboot 实现动态修改接口返回值

目录 一、前言 二、动态修改接口返回结果实现方案总结 2.1 使用反射动态修改返回结果参数 2.1.1 认识反射 2.1.2 反射的作用 2.1.3 反射相关的类 2.1.4 反射实现接口参数动态修改实现思路 2.2 使用ControllerAdvice 注解动态修改返回结果参数​​​​​​​ 2.2.1 注解…

【C++算法】4.双指针_快乐数

文章目录 题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;图解&#xff1a; 题目链接&#xff1a; 202.快乐数 题目描述&#xff1a; 解法 根据题目来看&#xff0c;可能是无限循环&#xff0c;也可能是快乐数。因为就相当于下图&#xff1a; 无限循环可…

QT--基础

将默认提供的程序都注释上意义 0101.pro QT core gui #QT表示要引入的类库 core&#xff1a;核心库 gui&#xff1a;图形化界面库 #如果要使用其他库类中的相关函数&#xff0c;则需要加对应的库类后&#xff0c;才能使用 greaterThan(QT_MAJOR_VERSION, 4): QT wid…

AMD 矩阵核心

AMD matrix cores — ROCm Blogs 注意&#xff1a; 本文博客之前是 AMD lab notes 博客系列的一部分。 矩阵乘法是线性代数的一个基本方面&#xff0c;它在高性能计算&#xff08;HPC&#xff09;应用中是一个普遍的计算。自从 AMD 推出 CDNA 架构以来&#xff0c;广义矩阵乘法…

基于SpringBoot+Vue+MySQL的甜品店管理系统

系统展示 用户前台界面 管理员后台界面 系统背景 在数字化浪潮的推动下&#xff0c;甜品店行业也面临着转型与升级的需求。传统的线下经营模式已难以满足现代消费者对于便捷、高效购物体验的追求。为了提升运营效率、优化顾客体验&#xff0c;我们设计了一款基于SpringBoot后端…

Django基础-创建新项目,各文件作用

学习Django的前置知识&#xff1a; python基本语法&#xff1a;需要掌握Python中的变量、循环、条件判断、函数等基本概念。面向对象编程&#xff08;OOP&#xff09;&#xff1a;Django的核心架构基于面向对象编程&#xff0c;许多功能&#xff08;如模型和视图&#xff09;依…

黑神话悟空小西天

游戏里我们一开始就出现一个很可爱的小和尚&#xff0c;当脚步声传来&#xff0c;小和尚化身为一尊弥勒佛&#xff0c;而这尊弥勒佛的大小和位置都在说&#xff0c;这里没有弥勒佛的位置。 随后天命人进入一片雪地&#xff0c;遇到了赤尻马猴&#xff0c;打跑赤尻马猴&#xff…

C++_unordered系列关联式容器(哈希)

unordered系列关联式容器&#xff0c;我们曾在C_map_set详解一文中浅浅的提了几句。今天我们来详细谈谈 本身在C11之前是没有unordered系列关联式容器的&#xff0c;unordered系列与普通的map、set的核心功能重叠度达到了90%&#xff0c;他们最大的不同就是底层结构的不同&…

AVL树(平衡二叉树)的介绍以及相关构建

欢迎光临 &#xff1a; 羑悻的小杀马特-CSDN博客 目录 一AVL树的介绍&#xff1a; 二AVL树的实现&#xff1a; 1结构框架&#xff1a; 2节点的插入&#xff1a; 旋转&#xff1a; 21左单旋&#xff1a; 2.1.1左单旋介绍及步骤&#xff1a; 2.1.2左单旋代码实…

【JavaSE系列】IO流

目录 前言 一、IO流概述 二、IO流体系结构 三、File相关的流 1. FileInputStream 2. FileOutputStream 3. FileReader 4. FileWriter 四、缓冲流 五、转换流 1. InputStreamReader 2. OutputStreamWriter 六、数据流 七、对象流 八、打印流 九、标准输入输出流…

C++学习9.28

1> 创建一个新项目&#xff0c;将默认提供的程序都注释上意义 por QT core gui #QT表示引入的类库 core:核心库例如IO操作在该库中 gui:图形化显示库 #如果要使用其他类库中的相关函数&#xff0c;就需要调用相关类库后&#xff0c;才能加以使用greaterThan(Q…

c++926

1.什么是虚函数&#xff1f;什么是纯虚函数&#xff1f; 虚函数&#xff1a;被virtual关键字修饰的成员函数&#xff0c;用于实现多态性&#xff0c;通过基类访问派生类的函数。纯虚函数&#xff1a;在虚函数后面添加0&#xff0c;只有声明而没有实现&#xff0c;需要派生类提…

天龙八部怀旧单机微改人面桃花+安装教程+GM工具+虚拟机一键端

今天给大家带来一款单机游戏的架设&#xff1a;天龙八部怀旧单机微改人面桃花。 另外&#xff1a;本人承接各种游戏架设&#xff08;单机联网&#xff09; 本人为了学习和研究软件内含的设计思想和原理&#xff0c;带了架设教程仅供娱乐。 教程是本人亲自搭建成功的&#xf…

图说数集相等定义表明“R各元x的对应x+0.0001的全体=R“是几百年重大错误

黄小宁 设集A&#xff5b;x&#xff5d;表A各元均由x代表&#xff0c;&#xff5b;x&#xff5d;中变量x的变域是A。其余类推。因各数x可是数轴上点的坐标故x∈R变为实数yx1的几何意义可是&#xff1a;一维空间“管道”g内R轴上的质点x∈R(x是点的坐标)沿“管道”g平移变为点y…

红队信息搜集扫描使用

红队信息搜集扫描使用 红队行动中需要工具化一些常用攻击&#xff0c;所以学习一下 nmap 等的常规使用&#xff0c;提供灵感 nmap 帮助 nmap --help主机扫描 Scan and no port scan&#xff08;扫描但不端口扫描&#xff09;。-sn 在老版本中是 -sP&#xff0c;P的含义是 P…