从底层源码看解析go语言的channel实现

go的channel用法非常简单,几句话说一下,会的不用看,往下走。

1.开启channel:

//没有缓存的channel,直接中转
c:= make(chan 类型)
//有n个【类型】位置作为缓存的channel,先拷贝进缓存,再中转
c:= make(chan 类型, n)
//对不带缓冲的 channel 进行的操作实际上可以看作“同步模式”
//带缓冲的则称为“异步模式”

//只读channel。
readC:= make(<-chan 类型)
//只写channel。
writeC:=make(chan<- 类型)

//当然只读没啥用,一般使用正常channel给只读/写channel传参
func rf(rC <-chan int)
func wf(wC chan<- int)

c:= make(chan 类型)
rf(c)
wf(c)


2.关闭channel:
close(c)

3.使用channel:
//往c里面写,写不进去就等着
c<-a

//从c里面拿,ok可以判断c关没关
b,ok:=<-c

//channel和打开文件不同,close函数是不必要调用的。
//就是说channel不用必须关闭,channel是变量级别的。
//回收可以让go语言的gc自己控制。

//channel不是值类型的,是引用类型的,就是说空值就是nil。

//close函数关闭channel只是关闭channel的入口。
//调用close后就不能再往里面写东西了,但是还是可以拿数据的。
//拿完为止,a,ok:=<-c第二个变量都会是true。

channel 是 Go 语言与众不同的特性之一,可以说只要接触go的人一定会接触channel,这个玩意大家肯定都会用,开始往深里思考。

  • 什么时候能用呢,大量并发的时候可以么?
  • 这个的锁粒度大不大,直接使用锁会更快么?
  • 单channel只能是单读单写么,多个读多个写对性能有影响么?
  • 并发肯定是会有协程调度的,那channel卡住了,会不会使协程调度更加频繁?
  • channel带不带缓冲区对会go的gc有影响么?

如果只是想项目能跑就行,那确实不用管,但是如果专注于性能,往深里一想,问题很多啊!所以不搞懂channel的底层实现,这些问题根本就不好考虑。

并发问题与CSP 模型

先从理论开始了解。

并发问题一般有下面这几种:

  1. 数据竞争。简单来说就是两个或多个线程同时读写某个变量,造成了预料之外的结果。
  2. 原子性。在一个定义好的上下文里,原子性操作不可分割。上下文的定义非常重要。有些代码,你在程序里看起来是原子的,如最简单的 i++,但在机器层面看来,这条语句通常需要几条指令来完成(Load,Incr,Store),不是不可分割的,也就不是原子性的。原子性可以让我们放心地构造并发安全的程序。
  3. 内存访问同步。代码中需要控制同时只有一个线程访问的区域称为临界区。Go 语言中一般使用 sync 包里的 Mutex 来完成同步访问控制。锁一般会带来比较大的性能开销,因此一般要考虑加锁的区域是否会频繁进入、锁的粒度如何控制等问题。
  4. 死锁。在一个死锁的程序里,每个线程都在等待其他线程,形成了一个首尾相连的尴尬局面,程序无法继续运行下去。
  5. 活锁。想象一下,你走在一条小路上,一个人迎面走来。你往左边走,想避开他;他做了相反的事情,他往右边走,结果两个都过不了。之后,两个人又都想从原来自己相反的方向走,还是同样的结果。这就是活锁,看起来都像在工作,但工作进度就是无法前进。
  6. 饥饿。并发的线程不能获取它所需要的资源以进行下一步的工作。通常是有一个非常贪婪的线程,长时间占据资源不释放,导致其他线程无法获得资源。

那么go是怎么解决并发上的如此繁多的问题呢,答案是CSP模型,CSP 经常被认为是 Go 在并发编程上成功的关键因素。

CSP 全称是 “Communicating Sequential Processes”,被 Tony Hoare 在1978年发表的论文中提出。是用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。

有意思的是,在那篇文章发表的时代,人们正在研究模块化编程的思想,该不该用 goto 语句在当时是最激烈的议题。彼时,面向对象编程的思想正在崛起,几乎没什么人关心并发编程。

这篇文章在当时并没有显出它应有的光辉。

在文章中,CSP 也是一门自定义的编程语言,作者定义了输入输出语句,用于 processes 间的通信(communicatiton)。processes 被认为是需要输入驱动,并且产生输出,供其他 processes 消费,processes 可以是进程、线程、甚至是代码块。输入命令是:!,用来向 processes 写入;输出是:?,用来从 processes 读出。Hoare 还提出了一个 -> 命令,如果 -> 左边的语句返回 false,那它右边的语句就不会执行。通过这些输入输出命令,Hoare 证明了如果一门编程语言中把 processes 间的通信看得第一等重要,那么并发编程的问题就会变得简单。

这篇论文对Go语言并发设计产生了巨大影响,最后设计者通过引入 channel 这种新的类型,来实现 CSP 的思想。channel是并发安全的,就是多线程同时操作也没问题的,插入和取数据都是原子性的,肯定是要加锁的,channel这个结构的理论模型就是csp模型。

其实从实际上出发,go语言并没有完全实现了CSP模型的所有理论,仅仅是借用了 process和channel这两个概念。process是在go语言上的表现就是 goroutine 是实际并发执行的实体,每个实体之间是通过channel通讯来实现数据共享。

Golang实现了 CSP 并发模型做为并发基础,底层使用goroutine做为并发实体,goroutine非常轻量级可以创建几十万个实体。实体间通过 channel 继续匿名消息传递使之解耦,在语言层面实现了自动调度,这样屏蔽了很多内部细节,对外提供简单的语法关键字,大大简化了并发编程的思维转换和管理线程的复杂性。

channel实现原理

又到了怎么绕都绕不开的源码环节了。

channel的底层实现有几个最重要的块:

  • channel的数据结构
  • channel的初始化方法
  • channel的send方法(发送数据方法)
  • channel的recv方法(获取数据方法)
  • channel的close方法(不是说不用调用??那也不是不重要,看吧)

channel的数据结构

type hchan struct {
 qcount   uint           // total data in the queue
                             // chan里元素数量
 dataqsiz uint           // size of the circular queue
                             // chan 底层循环数组的长度
 buf      unsafe.Pointer // points to an array of dataqsiz elements
                             // 指向底层循环数组的指针,只针对有缓冲的 channel
                             // 没有缓冲的用不到存储
 elemsize uint16         // chan中元素大小
 closed   uint32         // 关闭标志,是否已经close
 elemtype *_type         // element type
                             // chan元素类型
 sendx    uint           // send index
                             // send在buf中索引,就是已发送元素在循环数组中的索引
 recvx    uint           // receive index
                             // recv在buf中索引,就是已接收元素在循环数组中的索引
 recvq    waitq          // list of recv waiters
                             // 等待接收数据的 goroutine 队列
 sendq    waitq          // list of send waiters
                             // 等待发送数据的 goroutine 队列

 // lock protects all fields in hchan, as well as several
 // fields in sudogs blocked on this channel.
 //
 // Do not change another G's status while holding this lock
 // (in particular, do not ready a G), as this can deadlock
 // with stack shrinking.
 lock mutex              // 互斥锁,保护所有字段
}

非常清楚,go的channel结构实现的非常漂亮。甚至清楚到可以直接从结构就能模糊的想到如何实现channel。

来对结构体中的几个字段补充一些解释ÿ

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aiky哇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值