go-channel
设计原理
Go 提及的设计模式就是:不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。
- 共享内存方式:多个协程共享同一块内存,但是多个协程中读写变量是操作同一块内存,会产生多线程问题的并发问题,所以需要使用互斥锁来实现临界区的互斥访问,会大大影响效率
- 通信方式(go语言使用):channel通道当做通信的中间件队列,发送方 向channel
先入先出
channel收/发操作都遵循了先进先出的设计,它一共使用了3个队列来实现:
- 发操作:先向 Channel 发送数据的 Goroutine 会得到先发送数据的权利;(使用写队列
hchan.sendq)- 接收方会从缓冲区中读取数据,然后唤醒发送方,发送方会尝试向缓冲区写入数据,如果缓冲区已满会重新陷入休眠;
- 读操作:先从 Channel 读取数据的 Goroutine 会先接收到数据;(使用读队列
hchan.recvq)- 使用读队列:发送方会向缓冲区中写入数据,然后唤醒接收方,多个接收方会尝试从缓冲区中读取数据,如果没有读取到会重新陷入休眠;
无锁channel(结构体内还是有锁,好像暂未实现)
并发控制可由2种方式实现:
- 乐观锁:CAS(compare and swap)就是一种乐观锁,默认没有其他线程在修改,当本线程保存数据到内存时判断数据和修改前的原数据是否相同。
- 悲观锁:redis setnx就是一种悲观锁,默认有其他线程在修改,所以在其他线程拿数据前就阻塞,等待锁释放才能继续操作
乐观锁并没有锁这个变量,而是对原数据进行比较,所以乐观锁只是一种思想。无锁channel是使用了乐观锁思想实现的。
数据结构
runtime.hchan结构体
type hchan struct {qcount uintdataqsiz uintbuf unsafe.Pointerelemsize uint16closed uint32elemtype *_typesendx uintrecvx uintrecvq waitqsendq waitqlock mutex
}
- qcount:channel里的元素个数
- dataqsiz:Channel 中的循环队列的容量
- buf: Channel 的缓冲区数据指针
- elemsize:元素占内存的大小
- closed:channel的关闭状态
- elemtype:元素的类型元数据
- sendx: Channel 的发送操作处理到的位置
- recvx:Channel 的接收操作处理到的位置;
- recvq:接受队列(读队列),当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表
- sendq:发送队列(写队列),当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表
- lock:操作通道的锁,同一个时刻只有一个协程可以操作这个chan
队列中存的结构是runtime.sudog:
type sudog struct {// The following fields are protected by the hchan.lock of the// channel this sudog is blocking on. shrinkstack depends on// this for sudogs involved in channel ops.g *gnext *sudogprev *sudogelem unsafe.Pointer // data element (may point to stack)// The following fields are never accessed concurrently.// For channels, waitlink is only accessed by g.// For semaphores, all fields (including the ones above)// are only accessed when holding a semaRoot lock.acquiretime int64releasetime int64ticket uint32// isSelect indicates g is participating in a select, so// g.selectDone must be CAS'd to win the wake-up race.isSelect bool// success indicates whether communication over channel c// succeeded. It is true if the goroutine was awoken because a// value was delivered over channel c, and false if awoken// because c was closed.success boolparent *sudog // semaRoot binary treewaitlink *sudog // g.waiting list or semaRootwaittail *sudog // semaRootc *hchan // channel
}
- g:等待channel的goroutine指针
- channel:等待的哪个channel
- elem:等待发送/接收的缓冲区地址下标
channel类型
有缓冲区channel
hchan.buff指向一个数组地址,能存放数据,尽量避免了所有协程有阻塞
- 写操作:
- 如果缓冲区内有空间
hchan.qcount<hchan.dataqsiz,将数据放入缓冲区,hchan.sendx指向下一个数组下标,唤醒读队列hchan.recvq头部的协程。当前协程不阻塞,继续向下执行代码。 - 如果缓冲区内没有空间
hchan.qcount>=hchan.dataqsiz,当前goroutine阻塞(被挂起_GWating),新创建一个sudog,sudog.g指向当前goroutine,sudog变量塞进写队列hchan.sendq
- 如果缓冲区内有空间
- 读操作:和写操作差不多,只是操作
hchan.recvq和hchan.recvx
无缓冲区channel
hchan.buff是个nil值,没有数据存储的区域,肯定会出现阻塞现象
- 写操作:去
hchan.recvq队列中去获取一个正阻塞的协程sudog结构变量- 如果
hchan.recvq有数据,则根据sudog.g变量去唤醒协程,并向这个协程发送数据 - 如果
hchan.recvq有数据,则创建一个sudog结构体变量,sudog.g变量指向当前协程,放到hchan.sendq队列,当前goroutine阻塞(被挂起_GWating)
- 如果
- 读操作:和写操作差不多,只是操作
hchan.recvq
唤醒阻塞协程对channel做操作,都是由当前协程通知g0协程做调度
多路select
问题:为什么多个case被阻塞,说明当前g被加到了多个hchan.recvq或者hchan.sendq中,为什么只会执行一个case。
func SendBlock2() {c1 := make(chan int)c2 := make(chan int)// go不能放select后,因为执行顺序的问题,如果放后面在select就挂起了协程,导致没有创建这个协程,也就不可能唤醒当前协程,从而导致死锁go func() {time.Sleep(3 * time.Second)a := <-c1fmt.Println(a)}()select {case c1 <- 2:fmt.Println("case1")case c2 <- 3:fmt.Println("case2")}
}
上例的现象是:打印了case1或者case2,并不会两个都打印。
执行步骤:
- 执行case1时给c1加锁,执行case2时给c2加锁
- select乱序轮询
- g被加到c1和c2的
hchan.sendq中,c1和c2解锁允许其他协程操作这个channel,g被挂起等待 - 子协程命中唤醒主协程,命中case1,执行case1操作
- 再次对所有case的channel加锁(原因是下一步)
- 去c1,c2的
recvq和sendq遍历删除绑定了当前协程的sudog,因为删除了队列中的等待g,所以g不会被重新唤醒,case2就再也不命中。 - c1,c2再次解锁
- select结束
使用语法
创建channel
// 方法1,没有分配地址,无法读写chan
var c chan Type// 方法2,分配了地址,设置了size就是有缓冲channel,反之是无缓冲地址
c := make(chan Type [, size])
写channel
c := make(chan Type [, size])// 方法1
c <- val// 方法2
select {case c<-2:// 下一个case c<-2://业务逻辑default://可以避免阻塞
}
读channel
c := make(chan Type [, size])// 方法1
t := <-c// 方法2
t,ok := <-c // ok==false表明,chan被关闭// 方法3
select {case <-c://业务逻辑default://可以避免阻塞
}// 方法4
for element := range c {fmt.Println("chan element:", element)
}
关闭channel
close(c)
具体案例
有缓冲区channel
func SendBlock1() {// 创建缓冲区容量是3的通道c := make(chan int, 3)defer close(c)// 创建4个协程往通道里写,会有一个协程阻塞等待for i := 0; i < 4; i++ {go func(i int) {c <- ifmt.Printf("i=%d成功插入chan\n", i)}(i) // 如果i不使用传参方式,而是使用闭包函数,那么就会发生数据逃逸,i会被存到堆中,栈帧上的i变成指针指向堆,导致协程里的i不一定打印0,1,2,3}time.Sleep(3 * time.Second)//打印,2协程阻塞等待//i=3成功插入chan//i=0成功插入chan//i=1成功插入chan
}
无缓冲区channel
func SendBlock() {c := make(chan int)defer close(c)for i := 0; i < 4; i++ {// 如果i不使用传参方式,而是使用闭包函数,那么就会发生数据逃逸,i会被存到堆中,栈帧上的i变成指针指向堆,导致协程里的i不一定打印0,1,2,3go func(i int) {c <- ifmt.Printf("i=%d成功插入chan\n", i)}(i)}time.Sleep(3 * time.Second)//没有任何打印,因为hchan.recvq没有协程可以唤醒}
使用注意
- 对一个关闭的channel发送值 panic
- 对一个关闭的channel接收值,会一直读取成功,直到管道内数据为空
- 对一个关闭的并且没有值的管道执行接收操作,会得到对应类型的空值
- 关闭一个已关闭的通道会导致panic
- 关闭一个chan,会向所有正在监听这个chan的协程都发送一个空元素(元素类型取决于你的chan类型)
死锁:
func f1(channel chan int) {time.Sleep(6 * time.Second)channel <- 20//close(channel)
}func main() {channel := make(chan int)//例1 不会死锁,因为读写都只进行了一次之后就结算了go func() {time.Sleep(6 * time.Second)channel <- 20}()fmt.Println(<-channel) // 主协程会阻塞等待管道进入数据//例2 不会死锁,因为读写都只进行了一次之后就结算了go f1(channel)fmt.Println(<-channel)//例3 不会死锁,因为读写都只进行了一次之后就结算了go func(channel chan int) {channel <- 20}(channel)fmt.Println(<-channel)//例4 会死锁,主协程会一直等待子进程写入,无法退出,此时需要在子协程加入close(channel),表明自己不会在对协程做操作了go func(channel chan int) {channel <- 20// close(channel) 加上则不会死锁}(channel)for element := range channel {fmt.Println(element)}//例子5,结果是等待3秒后,其中一个消费协程会被死锁,因为他一直在等待channel的数据进入channel := make(chan int, 3)wg := sync.WaitGroup{}wg.Add(3)go func() {defer wg.Done()//fmt.Println("子协程1")fmt.Println("子协程1抢到的" + strconv.Itoa(<-channel))}()go func() {defer wg.Done()//fmt.Println("子协程2")fmt.Println("子协程2抢到的" + strconv.Itoa(<-channel))}()go func() {defer wg.Done()channel <- 20for i := 0; i < 3; i++ {time.Sleep(time.Second)}}()//channel <- 21wg.Wait()
}
相关文章:
go-channel
设计原理 Go 提及的设计模式就是:不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。 共享内存方式:多个协程共享同一块内存,但是多个协程中读写变量是操作同一块内存,会产生多线程问题的并发问题&am…...
K8s操作命令
生命周期管理 1. 创建 1. 创建资源 kubectl run 创建并运行一个或多个容器镜像。*创建一个deployment或job来管理容器*。 语法:kubectl run NAME --imageimage [–env“keyvalue”] [–portport] [–replicasreplicas] [–dry-runbool] [–overridesinline-jso…...
【MySQL】 MySQL数据库基础
文章目录 🐱👓数据库的操作📌显示当前的数据库📌创建数据库🎈语法:🎈语法说明🎈示例: 🌴使用数据库🎋删除数据库🐱🏍语…...
vscode 下载安装
vscode 下载安装常用插件 vscode 官网: https://code.visualstudio.com/ 点击右上角 Download 进入下载选择页面 选择自己使用操作对应 CPU 架构 下载 本文使用 x86 架构 64位 windows 系统为例 跳转下载页面 自动 开始下载 下载不开始?试试这个直…...
springboot对接postgres
安装postgres 注意:下述链接方式会自动创建数据库steven_russell,若需要创建其他数据库,可以手动执行命令创建数据库 docker run --name postgres \ -p 5432:5432 \ -e POSTGRES_USERsteven_russell \ -e POSTGRES_PASSWORD123456 \ -itd --privilegedtrue postgre…...
[python 刷题] 242 Valid Anagram
[python 刷题] 242 Valid Anagram 题目: Given two strings s and t, return true if t is an anagram of s, and false otherwise. An Anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the o…...
算法通过村第七关-树(递归/二叉树遍历)青铜笔记|手撕递归
文章目录 前言1. 递归的特征2. 如何写出好的递归3. 怎么看懂递归的代码总结 前言 提示:我们生活在24小时不眠不休的社会里但是没有24小时不眠不休的身体有些东西必须舍弃 -- 马特海格 这一关,我看要谈论的是递归问题,说到它就牵扯到很多问题了…...
#循循渐进学51单片机#点亮你的LED#not.2
1、深刻理解电容的意义,并且在今后的电路学习过程中要多多注意参考别人电路中去耦电路的处理方法,积累经验。 1)电容缓冲电压,抗电磁干扰; 2)低频率电容,一般用的最多的是钽电容,电…...
基于Java+SpringBoot+Vue+uniapp点餐小程序(亮点:协同过滤算法、会员系统,购物车结算、在线聊天)
校园点餐小程序 一、前言二、我的优势2.1 自己的网站2.2 自己的小程序(小蔡coding)2.3 有保障的售后2.4 福利 三、开发环境与技术3.1 MySQL数据库3.2 Vue前端技术3.3 Spring Boot框架3.4 微信小程序 四、功能设计4.1 系统功能结构设计4.2 主要功能描述 五…...
深度学习-全连接神经网络-详解梯度下降从BGD到ADAM - [北邮鲁鹏]
文章目录 参考文章及视频导言梯度下降的原理、过程一、什么是梯度下降?二、梯度下降的运行过程 批量梯度下降法(BGD)随机梯度下降法(SGD)小批量梯度下降法(MBGD)梯度算法的改进梯度下降算法存在的问题动量法(Momentum)目标改进思想为什么有效动量法还有什么效果&…...
数据结构--二叉排序树
目录 二叉排序树的定义 二叉排序树的查找 二叉排序树的插入 二叉排序树的构造 二叉排序树的删除 查找效率分析 回顾 二叉排序树的定义 二叉排序树的查找 查找成功的情况 查找失败的情况 二叉排序树的插入 注意 (1)二叉排序树不允许出现重复的值…...
Python | 根据子列表中的第二个元素对列表进行排序
在本文中,我们将学习如何根据主列表中存在的子列表的第二个元素对任何列表进行排序。 比如 Input : [[‘rishav’, 10], [‘akash’, 5], [‘ram’, 20], [‘gaurav’, 15]] Output : [[‘akash’, 5], [‘rishav’, 10], [‘gaurav’, 15], [‘ram’, 20]] Input …...
qsort函数详细讲解以及利用冒泡排序模拟实现qsort函数
个人主页:点我进入主页 专栏分类:C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 欢迎大家点赞,评论,收藏。 一起努力,一起奔赴大厂。 目录 1.qsort函数 1.1qsort函数的参数 …...
C++QT day6
1> 将之前定义的栈类和队列类都实现成模板类 栈: #include <iostream> #define MAX 128 using namespace std; template<typename T> class Stack_s { private:T *pnew T[MAX];//栈的数组int top;//记录栈顶的变量 public://构造函数Stack_s(int t…...
List与ArrayList
目录 一、List及其使用 1.1 List的概念 1.2 常见接口的介绍 1.3 List的使用 二、线性表和顺序表 2.1 线性表 2.2 顺序表 三、ArrayList介绍 四、ArrayList的使用 4.1 ArrayList构造 4.2 ArrayList的常用方法 4.3 ArrayList的遍历 4.4 ArrayList的扩容机制 五、ArrayList的具…...
【C++】特殊类的设计
文章目录 1. 设计一个类, 不能被拷贝2. 设计一个类, 不能被继承3. 设计一个类, 只能在堆上创建对象3. 设计一个类, 只能在栈上创建对象4. 创建一个类, 只能创建一个对象(单例模式)饿汉模式懒汉模式 1. 设计一个类, 不能被拷贝 💕 C98方式: 在C11之前&a…...
机器学习:PCA(Principal Component Analysis主成分)降维
参考:PCA降维原理 操作步骤与优缺点_TranSad的博客-CSDN博客 PCA降维算法_偶尔努力翻身的咸鱼的博客-CSDN博客 需要提前了解的数学知识: 一、PCA的主要思想 PCA,即主成分分析方法,是一种使用最广泛的数据降维算法。PCA的主要思想…...
linux服务器slab缓存回收方案设计
背景 自己写的回收slab内存ko,insmod报错“shrink_slab:unknown symbol _x86_indirect_thunk_rax(err 0)””; 分析 1.名词解释 在 x86 架构中,函数调用通常使用 call 指令来直接跳转到目标函数的地址。但是,当需要通过函数指针或动态链接调用函数时,就需要使用__x86_…...
Apache Spark 的基本概念
Apache Spark 是一种快速、可扩展、通用的数据处理引擎。它是一种基于内存的计算框架,支持分布式数据处理、机器学习、图形计算等多种计算任务。与传统的 Hadoop MapReduce 相比,Spark 具有更高的性能和更广泛的应用场景。 Spark 中的基本概念包括&…...
通讯协议介绍CoAP 协议解析
目录 1 通讯协议 2 TCP/IP 网络模型 2.1 TCP协议 2.1.1 TCP 连接过程 2.1.2 TCP 断开连接 2.1.3 TCP协议特点 2.2 UDP协议 2.2.1 UDP 协议特点 3 应用层协议简介 3.1 HTTP 协议 3.2 CoAP 协议 3.3 MQTT 协议 4 CoAP 协议详解 4.1 REST 风格 4.2 CoAP 首部分析 4…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...
