文章目录

  • 1. channel的基本使用
  • 2. 内存和通信
  • 3. channel底层设计
  • 4. channel如何发送数据
  • 5. channel如何接收数据

1. channel的基本使用

  1. 如何声明管道?

我们首先可以使用make(chan 管道中存放的数据类型,缓冲区大小)来进行声明,分为两大类,一类是带缓冲区的管道,一类是不带缓冲区的管道(不带缓冲区的管道在声明的时候省略缓冲区大小的参数即可),具体声明方法如下代码所示:

make(chan int)   // 无缓冲区
make(chan bool, 0) // 不带缓冲区
make(chan string, 2) // 带缓冲区(2个)
  1. 基本用法

管道其实就是一种通信方式,本质上用来发送和接受数据,那么关于它的主要用法当然也就是这两种,具体使用如下代码所示:

ch <- x   // 发送数据x
x =<- ch  // 将管道中接受的数据赋值给x
<-ch      // 接受管道中的数据并丢弃

需要注意的是如果是没有缓冲区的管道,是无法向管道中塞数据的,除非此时有一个正在运行的协程等待接受这个数据,才能向无缓冲区的管道中放数据。(有缓冲区的管道就像是一条马路上有一个仓库,这个仓库可以用来放需要传送的东西,当没有这个仓库且此时马路对面没有接收者的时候,这条马路(这个管道)不允许发送货物(数据))。

2. 内存和通信

首先我们要记住一句话:不要通过共享内存的方式进行通信,二是应该通过通信的方式共享内存。我们来一段代码举例一下什么是使用共享内存的方式来通信,如下:

func watch(p *int) {for true {if *p == 1 {fmt.Println(a...:"hello")break}}
}func main() {i := 0go watch(&i)time.Sleep(time.Second)i = 1time.Sleep(time.Second)
}// output:
// hello

以上代码就是通过共享内存来通信的,主函数向watch发送了变量的地址,watch通过不断地查看该地址的变量是否是1然后输出并跳出循环。

那么如果使用管道来进行通信,代码应该是什么样子的呢?

func watch(c chan int) {​    if <-c == 1 {        fmt.Println("hello")    }
}​func main() {    c := make(chan int)go watch(c)​    time.Sleep(time.Second)​    c <- 1​time.Sleep(time.Second)
}// output:
// hello

我们发现我们使用了一个不带缓冲区的管道,且因为管道是一种通信的模型,当没有数据的时候,管道是阻塞状态的,所以会发现在以上代码中我们没有使用死循环。显然使用管道的方式避免了watch函数多次循环,节约了系统资源。

我们大致了解了什么是通过共享内存来进行通信,什么是通过通信方式来共享之后,大致给出为什么使用通信来共享内存的原因:

  • 能够避免协程竞争和数据冲突的问题(毕竟共享内存使得多个协程将会读取同一块数据,使用共享内存通信必然要加锁,而管道在某种意义上可以认为是无锁的)
  • 更高级的抽象,降低开发难度,增加程序可读性(就像我们举例中的,通信的方式避免了轮询查看)
  • 模块之间更容易解耦,增强扩展性和可维护性(共享内存显然不适合分布式环境,因为共享内存要求至少在同一个硬盘上)

3. channel底层设计

chanel作为一个通信管道,我们在上文中也基本了解了它的功能,他需要发送数据和接受数据,特别是如果这是一个带缓冲区的管道的话,它首先需要一个用于存放数据的缓冲区,除此以外,当channel缓冲区满的时候,如果某个发送方还像其发送数据,他就需要一个队列来存放该发送协程,接收方同理。故而,整体而言,一个channel需要三大块:中间缓冲区、发送等待队列和接受等待队列。

以上只是channel一个大致的形式,在go的底层,channel实质上是一个名为hchan的结构体,其中的具体内容如下:


前几个数据实际上是一个环形的缓存队列,这种环形缓存可以大幅度降低回收内存的开销。我们将其拿出来理解一下,如下图所示。

成员变量 含义
qcount 环形队列中存放数据的数量
dataqsiz 环形队列的容量
buf 指向环形队列
elemsize 每个数据的大小
elemtype 数据的类型

除此以外剩余的几个结构体,我们找到其相关源码如下,实际上waitq类型(该类型中的elem是用于接收数据的变量的指针)的两个变量recvq和sendq都是链表,功能上是一个存放协程的队列,sendx和recvx指的是目前工作到发送/接受链表的第几个,是一个游标。

还有一个关键成员mutex,这个互斥锁并不是用于排队发送/接受数据,是用于保护hchan结构体中的所有成员,所有协程想要操作hchan中的都需要加锁。所以channel本质上并不是无锁的,就其本身的构成来说。但是由于channel的锁只是在外部协程向hchan中塞数据或者取数据拿一下才会用到,所以即使使用channel也可以实现高并发的通信,在这种意义上我们也可以说channel是无锁的。

除此以外,不知道读者发现了没有,其中还有一个成员closed,他是channel的一个状态值,0的时候表示开启,1的时候表示关闭。

以上就是channel中的全部成员了,我们看一张总结的图如下:

4. channel如何发送数据

文章一开始我们使用channel演示传输和发送数据,在这部分我们了解一下channel如何发送数据,在代码使用的时候我们直接使用了c<-关键字来发送数据,它在go中其实是一个语法糖,在编译阶段,编译器就会把c<-转化为runtime.chansend1(),如果去看go中的源码,你就会发现这个chansend1其实回去调用chansend方法。

我们可以将channel发送的情形分为以下几种:

  • 直接发送
  • 放入缓存
  • 休眠等待

我们接下来分点来讨论一下这几种情形。

  1. 直接发送

在发送数据前,此时有协程在休眠等待介绍。其实就是此时管道中的等待接受数据的协程还没有检测到数据的到来,此时该协程就会休眠等待。此时结构体中的环形缓存没有数据,此时就会将这个等待数据的协程放入管道对应的hchan结构体中的接收队列中去,如果此时有数据到来就从管道结构体的接收队列中取出一个等待接收的协程,直接将其拷贝给协程的接收变量(不用将数据放入环形缓存区中),唤醒该协程。

  1. 放入缓存

此时没有休眠等待的协程,管道结构体中的环形缓存中还有缓存空间,如果此时有数据接收就直接将数据放入缓存。实现的时候首先获取环形队列中可存入的缓存地址,然后将数据存入该地址,同时维护相关的索引。这也是为什么说channel是无锁的原因,只要缓存区还有空间,就不会存在阻塞。

  1. 休眠等待

此时没有协程休眠等待,且环形队列的缓存已经满了,此时发送协程进入发送队列,休眠等待。在实现上而言,发送协程将自己包装为一个sudog,然后放入发送队列中去,最后休眠并将hchan中的互斥锁进行解锁。

5. channel如何接收数据

和channal发送数据中很相似,用于接收数据的<-c其实也是一个语法糖,在编译阶段i<-c会转化为runtime.chanrecv1(),除此以外,还有一个ok参数可以用来接收管道中的数据,可以用于判断到底有没有从管道中接收到数据,这种i,ok -< c在编译阶段将会转换为runtime.chanrecv2()。最终两者都会调用chanrecv()

我们可以将channel接收数据的情形分为以下四种:

  • 有等待的协程,从协程接收
  • 有等待的协程,从缓存接收
  • 接收缓存
  • 阻塞接收

我们接下来分点来了解一下:

  1. 有等待的协程,从协程接收

在数据接受之前,已经有协程在休眠等待发送了,且这个channe没有缓存,此时接收协程将数据直接从协程拷贝过来并唤醒协程。从实现上来讲,接收协程需要判断此时是否有协程在发送队列等待(进入recv方法)以及环形缓存中是否有数据,如果两者都满足就将发送队列中的协程中的数据取出来并将其唤醒。

  1. 有等待的协程,从缓存接收

在接受数据之前,已经有协程在休眠等待发送,且此时channel的环形队列中有缓存,那么接收协程从环形队列缓存区取出一个数据,将休眠协程的发送协程的数据放入缓存并唤醒该发送协程。从实现上来讲,接收协程首先判断是否有协程在发送队列中等待,如果有就进入recv方法中,然后再判断Channel是否有缓存,有的话从缓存中取走一个数据,并将发送协程的数据放入缓存并唤醒该协程。

  1. 接收缓存

当没有协程在休眠等待,但是环形队列缓存区中有内容,就从缓存中取走数据。从实现来讲就是判断发送队列中是否没有协程在等待且此时channel缓存有内容,就直接从缓存中取一个数据。

  1. 阻塞接收

此时没有协程在休眠等待,且没有缓存,接收协程进入接收队列中等待接收数据。在实现上来说就是判断是否没有协程在发送队列等待以及此时缓存区没数据,如是接收协程就将自己包装成sudog,把自己放入接受等待队列中,并休眠。

go中高并发下的通信方式:channel管道的底层原理相关推荐

  1. golang 重要知识:channel 用法和底层原理

    前言 channel 是 goroutine 与 goroutine 之间通信的重要桥梁,借助 channel,我们能很轻易的写出一个多协程通信程序.今天,我们就来看看这个 channel 的常用用法 ...

  2. channel 的底层原理

    三.channel 的底层原理 前面提及过 channel 创建后返回了 hchan 结构体,现在我们来研究下这个结构体,它的主要字段如下: type hchan struct {qcount uin ...

  3. 专科 java转go 翱翔之路(二)基础语法:匿名组合,方法,接口,map,json,异常处理,channel管道,select用法

    2.4 面向对象编程 2.4.1匿名组合 type Person struct {id intname stringage int }type Student struct {Person //只有类 ...

  4. channelinactive触发后不关闭channel_go那些事儿|channel使用及其实现原理

    目录 channel背景 channel基本用法 channel应用场景 channel实现原理 channel数据结构 channel实现方式 channel注意事项 闲聊 欢迎加入我的公众号[迈莫 ...

  5. pipe 半双工_pipe 半双工_Linux管道PIPE的原理和应用

    Linux中进程的通信方式有信号,管道,共享内存,消息队列socket等.其中管道是*nix系统进程间通信的最古老形式,所有*nix都提供这种通信方式.管道是一种半双工的通信机制,也就是说,它只能一端 ...

  6. VC++下命名管道编程的原理及实现

    概述 管道(Pipe)实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机.命名管道(Named Pipes)是在管道服务器和一台或多台管道客户机之间进行 ...

  7. golang channel 管道

    channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication). 它的操作符是箭头 <- . ch <- v ...

  8. 卷毛0基础学习Golang-并发编程-03 channel管道

    channel channel是Go语言中的一个核心类型,可以把它看成管道.并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度. channel是一个数据类型,主 ...

  9. 进程间的通信方式(管道,消息队列,共享内存,信号)

    目录 了解 使用管道来通信 无名管道 有名管道 消息队列 key 键值生成 共享内存 信号 信号概述 信号如何携带消息 信号发送函数: 信号量 了解 创建进程后实现父子通讯的连接. 我们希望有一个管道 ...

最新文章

  1. Linux cut命令
  2. google guava工具包collect包HashMultiMap基本用法
  3. 晶科能源坐稳全球光伏组件制造商“头把交椅”
  4. JBoss 系列八十五: JBoss Modules 简单介绍
  5. C# WinForm自定义拖动窗体
  6. php 正则表达式 匹配中日韩字符(GBK)
  7. android的padding属性,以编程方式获取android:padding属性
  8. 打印机共享无法正常打印的处理思路
  9. android 中在CMD中查看sqlite
  10. Zygo保存zxg(Zemax File)文件(光学领域知道Zygo的一定要看)
  11. 苹果手机如何调节屏幕彩色(对于百度提供的方式行不通时此方法必有用)
  12. C语言联合体(union)的使用方法及其本质-union
  13. 电动汽车仿真系列-基于动态规划的混合动力汽车能量管理
  14. html5远程桌面 微软,微软正在测试远程桌面HTML5网页版本客户端!
  15. spo0lsv病毒分析
  16. 计算机整体硬盘销毁,如何完全销毁硬盘上的数据?
  17. 阿里云薛冰洋:边缘云自动化测试解决方案—TestMaster
  18. python123app_Python实现iOS APP 自动化打包
  19. CodeWarrior相关概述
  20. 计算机非全日制有用吗,计算机在职研究生还会有用吗?

热门文章

  1. 总结过去展望未来_未来的发生速度将比过去快得多
  2. linux 解压tar.lz文件
  3. k8s部署Traefik
  4. Maven的聚合和继承(六)
  5. Tensorflow - from keras.layers import LeakyReLU 实例源码
  6. Task12:哈希表
  7. 3T 硬盘读写速度小测
  8. 噪声:Practical Poissonian-Gaussian noise modeling and fitting for single-image raw-data
  9. 如何选择合适的固态继电器?
  10. 如何停止关闭您的Android手机的屏幕