GoLong的学习之路(二十二)进阶,语法之并发(go最重要的特点)(channel的主要用法)

这一章是接上一章内容继续,上一章说到协程也就是goroutine,如何使用它,这一张是讲一种数据结构。当然这个章节的数据结构非常重要。可以说这个数据结构就是为了方便协程,才制作出来的。

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。

为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

文章目录

  • channel序章
  • channel类型
    • channel的初始化
    • 操作channel
    • 无缓冲的通道
    • 有缓冲的通道
    • 多返回值模式
    • for range接收值
    • 单向通道

channel序章

Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡
通过通信共享内存而不是通过共享内存而实现通信

如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是 Go 语言中一种特有的类型。

var 变量名称 chan 元素类型
  • chan:是关键字
  • 元素类型:是指通道中传递元素的类型

比如:

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

channel零值
未初始化的通道类型变量其默认零值是nil

var ch chan int
fmt.Println(ch) // <nil>

channel的初始化

数据结构的初始化其实都差不多,都用make
声明的通道类型变量需要使用内置的make函数初始化之后才能使用。

make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的

ch4 := make(chan int)
ch5 := make(chan bool, 1)  // 声明一个缓冲区大小为1的通道

操作channel

通道共有发送(send)、接收(receive)关闭(close)三种操作。而发送和接收操作都使用<-符号。

定义通道

ch := make(chan int)

发送
将一个值发送到通道中。

ch <- 10 // 把10发送到ch中

接收
从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭
我们通过调用内置的close函数来关闭通道。

close(ch)

注意
一个通道值是可以被垃圾回收掉的。

通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的

关闭后的通道有以下特点:

  • 对一个关闭的通道再发送值就会导致 panic。
  • 对一个关闭的通道进行接收会一直获取值直到通道为空。
  • 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  • 关闭一个已经关闭的通道会导致 panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道

func main() {
	ch := make(chan int)
	ch <- 10
	fmt.Println("发送成功")
}

在这里插入图片描述

G 大写的报错。。。。
deadlock表示我们程序中的 goroutine 都被挂起导致程序死锁了。

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。

同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞

简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功

如何解决上述问题?
方案1:创建一个 goroutine 去接收值

var wait sync.WaitGroup

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)

}

func main() {
	ch := make(chan int)
	go recv(ch) // 创建一个 goroutine 从通道接收值
	ch <- 10
	fmt.Println("发送成功")
}

首先无缓冲通道ch上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时数字10才能发送成功,两个 goroutine 将继续执行。

相反,如果接收操作先执行,接收方所在的goroutine将阻塞,直到 main goroutine 中向该通道发送数字10。

使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道

有缓冲的通道

另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。

我们可以在使用 make 函数初始化通道时,可以为其指定通道的容量

func main() {
	ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
	ch <- 10
	fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。

当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

多返回值模式

当向通道中发送完数据时,我们可以通过close函数来关闭通道。

当一个通道被关闭后,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值。

通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个通道是否被关闭了呢?

value, ok := <- ch
  • value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
  • ok:通道ch关闭时返回 false,否则返回 true。

下面代码片段中的f2函数会循环从通道ch中接收所有值,直到通道被关闭后退出

func f2(ch chan int) {
	for {
		v, ok := <-ch
		if !ok {
			fmt.Println("通道已关闭")
			break
		}
		fmt.Printf("v:%#v ok:%#v\n", v, ok)
	}
}

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	f2(ch)
}

for range接收值

通常我们会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range改写后会很简洁。

func f3(ch chan int) {
	for v := range ch {
		fmt.Println(v)
	}
}

注意
目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。

单向通道

在某些场景下我们可能会将通道作为参数多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,比如限制通道在某个函数中只能执行发送或只能执行接收操作。

想象一下,我们现在有ProducerConsumer两个函数,其中Producer函数会返回一个通道,并且会持续将符合条件的数据发送至该通道,并在发送完成后将该通道关闭

Consumer函数的任务是从通道中接收值进行计算,这两个函数之间通过Processer函数返回的通道进行通信。

package main

import (
	"fmt"
)

// Producer 返回一个通道
// 并持续将符合条件的数据发送至返回的通道中
// 数据发送完成后会将返回的通道关闭
func Producer() chan int {
	ch := make(chan int, 2)
	// 创建一个新的goroutine执行发送数据的任务
	go func() {
		for i := 0; i < 10; i++ {
			if i%2 == 1 {
				ch <- i
			}
		}
		close(ch) // 任务完成后关闭通道
	}()

	return ch
}

// Consumer 从通道中接收数据进行计算
func Consumer(ch chan int) int {
	sum := 0
	for v := range ch {
		sum += v
	}
	return sum
}

func main() {
	ch := Producer()

	res := Consumer(ch)
	fmt.Println(res) // 25

}

大家可以调试一下,可有意思了,比线程有意思多了。

可以看出正常情况下Consumer函数中只会对通道进行接收操作,但是这不代表不可以在Consumer函数中对通道进行发送操作。

作为Producer函数的提供者,我们在返回通道的时候可能只希望调用方拿到返回的通道后只能对其进行接收操作。

然而这种方式我们没有办法阻止在Consumer函数中对通道进行发送操作

Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况

<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
  • 箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。
  • 另外对一个只接收通道执行close也是不允许的,因为默认通道的关闭操作应该由发送方来完成。

修改上述代码

// Producer2 返回一个接收通道
func Producer2() <-chan int {
	ch := make(chan int, 2)
	// 创建一个新的goroutine执行发送数据的任务
	go func() {
		for i := 0; i < 10; i++ {
			if i%2 == 1 {
				ch <- i
			}
		}
		close(ch) // 任务完成后关闭通道
	}()

	return ch
}

// Consumer2 参数为接收通道
func Consumer2(ch <-chan int) int {
	sum := 0
	for v := range ch {
		sum += v
	}
	return sum
}

func main() {
	ch2 := Producer2()
  
	res2 := Consumer2(ch2)
	fmt.Println(res2) // 25
}

Producer函数返回的是一个只接收通道,这就从代码层面限制了该函数返回的通道只能进行接收操作,保证了数据安全。

看到这个示例可能会觉着这样的限制是多余的,但是试想一下如果Producer函数可以在其他地方被其他人调用,你该如何限制他人不对该通道执行发送操作呢?

在函数传参及任何赋值操作中全向通道(正常通道)可以转换为单向通道,但是无法反向转换

var ch3 = make(chan int, 1)
ch3 <- 10
close(ch3)
Consumer2(ch3) // 函数传参时将ch3转为单向通道

var ch4 = make(chan int, 1)
ch4 <- 10
var ch5 <-chan int // 声明一个只接收通道ch5
ch5 = ch4          // 变量赋值时将ch4转为单向通道
<-ch5

在这里插入图片描述

对已经关闭的通道再执行 close 也会引发 panic

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

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

相关文章

echart宽度100px原因(解决el-tabs里的echarts图表宽度不自适应,只有100px问题)

目录 问题描述产生原因处理方法1.使用echart 的API —— resize()2.使用 v-if 总结 问题描述 项目中在el-tabs下面使用了图表&#xff0c;发现图表的宽度始终只有100px 产生原因 首先echart初始化的组件宽度设置了width: 100%&#xff0c;那么本来这个时候&#xff0c;echar…

Flutter android和ios闪屏页配置

一.概念理解 闪屏页 1.当点击app开始的一瞬间&#xff0c;所呈现出来的页面就是闪屏页。 2.为什么会有闪屏也&#xff0c;由于app启动需要加载代码&#xff0c;这个过程需要耗时&#xff0c;在没有加载完成之前&#xff0c;是看不到app真正的页面。所以app在没有完全加载完时…

计算摄像技术03 - 数字感光器件

一些计算摄像技术知识内容的整理&#xff1a;感光器件的发展过程、数字感光器件结构、数字感光器件的指标。 目录 一、感光器件的发展过程 二、数字感光器件结构 &#xff08;1&#xff09;CCD结构 ① 微透镜 ② 滤光片 ③ 感光层 电荷传输模式 &#xff08;2&#xff09;CMOS结…

nigix安装以及遇到的问题

Nginx配置 nginx双击闪退如何解决 修改配置文件 端口冲突&#xff0c;将端口改为90 Nginx 动静分离&#xff08;前端的代码单独运行&#xff09; 将html文件夹里面的东西放到nginx里面的HTMl文件夹里面 负载均衡&#xff08;轮询&#xff0c;权重&#xff0c;哈希&#xff…

三数之和(双指针)

15. 三数之和 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三…

企业如何落地搭建商业智能BI系统

随着新一代信息化、数字化技术的应用&#xff0c;引发了新一轮的科技革命&#xff0c;现代化社会和数字化的联系越来越紧密&#xff0c;数据也变成继土地、劳动力、资本、技术之后的第五大生产要素&#xff0c;这一切都表明世界已经找准未来方向&#xff0c;前沿科技也与落地并…

数据库 高阶语句

目录 数据库 高阶语句 使用select 语句&#xff0c;用order by来对进行排序 区间判断查询和去重查询 如何对结果进行分组查询group by语句 limit 限制输出的结果记录&#xff0c;查看表中的指定行 通配符 设置别名&#xff1a;alias 简写就是 as 使用select 语句&#x…

CVE-2023-0179-Nftables整型溢出

前言 Netfilter是一个用于Linux操作系统的网络数据包过滤框架&#xff0c;它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式&#xff0c;以实现网络安全、网络地址转换&#xff08;Network Address Transl…

19、Flink 的Table API 和 SQL 中的自定义函数及示例(3)

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…

2023年破圈:盘点11个新零售商业模式,永远不再打商业价格战

2023年破圈&#xff1a;盘点11个新零售商业模式&#xff0c;永远不再打商业价格战 前沿&#xff1a;纵观今年互联网各种类型项目&#xff0c;基本都是又短又快&#xff0c;但依然也有风靡的短跑冠军&#xff0c;那么互联网的项目能否跑的长久&#xff0c;是否是商业模式的原因&…

C++ —— map 和 multimap

一、map 1.介绍 1. map是关联容器&#xff0c;它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元 素。 2. 在map中&#xff0c;键值key通常用于排序和惟一地标识元素&#xff0c;而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同&am…

NAT协议

目录 NAT 前言 NAT地址转换表 NAT分类 前言 静态NAT 192.168.1.2访问200.1.1.2执行过程 动态NAT 192.168.1.2访问200.1.1.2执行过程 NAPT 192.168.1.2的5000端口访问200.1.1.2的80端口执行过程 基本命令 配置动态NAPT转换 定义内外网接口 配置NAPT 静态NAPT配置…

Linux基础【Linux知识贩卖机】

偶尔的停顿和修整&#xff0c;对于人生是非常必要的。 --随记 文章目录 Linux目录目录结构磁盘分区相关命令 相对路径和绝对路径 文件权限用户分类umask创建文件权限计算方法粘滞位 总结 Linux目录 目录结构 Linux 操作系统采用了一种层次化的目录结构&#xff0c;常被称为标…

使命担当 守护安全 | 中睿天下获全国海关信息中心感谢信

近日&#xff0c;全国海关信息中心向中睿天下发来感谢信&#xff0c;对中睿天下在2023年网络攻防演练专项活动中的大力支持和优异表现给予了高度赞扬。 中睿天下对此次任务高度重视&#xff0c;紧密围绕全国海关信息中心的行动要求&#xff0c;发挥自身优势有效整合资源&#x…

vivado时序分析-2时序分析关键概念

时序分析关键概念 1、最大和最小延迟分析 时序分析属静态验证 &#xff0c; 旨在验证在硬件上加载并运行设计后 &#xff0c; 其时序行为的可预测性。它会将各种制造和环境变化因素组合到延迟模型中并按时序角及其变化量加以分组&#xff0c; 将所有这些要素一并纳入考量范围。…

[动态规划] (十三) 简单多状态 LeetCode 740.删除并获得点数

[动态规划] (十三) 简单多状态: LeetCode 740.删除并获得点数 文章目录 [动态规划] (十三) 简单多状态: LeetCode 740.删除并获得点数题目解析解题思路状态表示状态转移方程初始化和填表顺序返回值 代码实现总结 740. 删除并获得点数 题目解析 (1) 给定一个整数数组。 (2) 选…

lvgl 转换和使用新字体

一、背景 如果lvgl 提供的默认字体不符合我们的显示要求&#xff0c;我们可以在网上下载开源字体&#xff0c;或者利用系统自带&#xff08;注意版权问题&#xff09;的字体文件转换lvgl 能识别和调用的字体。 或者为了压缩存储空间&#xff0c;某些字体我们只需要个别字符&…

【数据结构】堆排序和top-K问题

堆的实现源码 #define _CRT_SECURE_NO_WARNINGS#include <stdio.h> #include <stdlib.h> #include <time.h> #include <stdbool.h> #include <assert.h> typedef struct Heap {int* a;int size;int capacity; }Heap; void HeapInit(Heap* st) {…

nacos做服务配置和服务器发现

一、创建项目 1、创建一个spring-boot的项目 2、创建三个模块file、system、gateway模块 3、file和system分别配置启动信息,并且创建一个简单的控制器 server.port9000 spring.application.namefile server.servlet.context-path/file4、在根目录下引入依赖 <properties&g…

《UML和模式应用(原书第3版)》2024新修订译本部分截图

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 机械工业出版社即将在2024春节前后推出《UML和模式应用&#xff08;原书第3版&#xff09;》的典藏版。 受出版社委托&#xff0c;UMLChina审校了原中译本并做了一些修订。同比来说&a…