【go学习笔记】channel

channel是go独有的一个语言设计,也是go并发编程重要的组成部分。

今天来学习一下channel。

  • 一、channel介绍

  • 二、channel使用

  • 三、channel原理

  • 四、channel和java的对比

  • 五、总结

一、channel介绍

派生类型文章也有提到,channel是go内置的一种派生类型。

go 通过channel来进行goroutine之间的通信。

go推荐使用 CSP(communicating sequential processes)并发模型。

“不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存”

二、channel使用

2.1 channel声明

使用关键字chan声明channel:

make(chan 类型)

比如:

ch := make(chan string) 

2.2 channel的操作

  • 创建:创建channel,通过内置函数make。

  • 发送:将一个值放入到channel中,操作符为 ch<-val。

  • 接收:从 channel 中读取值,操作符为  val <- ch。

  • 关闭:关闭channel,通过内置函数close(ch)。

比如:

func otherG(ch chan string) {
    r := <- ch   //接收
    fmt.Println("接收成功,", r)
}
func main() {
    ch := make(chan string)
    go otherG(ch)
    ch <- "你好啊"   //发送
    fmt.Println("发送成功")
    close(ch)  //关闭
}

2.3 无缓冲 channel和有缓冲channel

  • 无缓冲channel:默认创建的就是无缓冲channel,channel容量为0。无法存储数据,操作需要阻塞,等待发送和接收的同时进行。

  • 有缓冲channel:即声明时带上channel的容量。容量大于0即为有缓冲channel。具备存储数据功能,类似于队列,先进先出,只有存储满了才会阻塞。比如:

ch := make(chan int, 5)

2.4 单向 channel

当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。

Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。

  • 只发送:chan<-

  • 只接收:<-chan

比如:

func otherG(ch <-chan string) {
    r := <- ch   //接收
    fmt.Println("接收成功,", r)
}
func main() {
    ch := make(chan string)
    go otherG(ch)
    ch <- "你好啊"   //发送
    fmt.Println("发送成功")
    close(ch)  //关闭
}

2.5 select

看到select,我们第一反应就是想到操作系统中关于多路复用模型的api。

操作系统中的select系统调用可以同时监听多个文件描述符的读写就绪事件。

类似操作系统中的select,go语言中的select可以同时监听多个channel的读写就绪事件。

select是go语言内置的一个关键字,类似switch,配合case使用,case中声明channel的读写就绪事件。 比如:

func selectFunc(channel1 chan string, channel2 chan int) {
 sendStr :="grandLine"
 select {
  case channel1 <- sendStr:
   fmt.Println("send data:",sendStr)
  case recvNum := <- channel2:
   fmt.Println("recv data:",recvNum)
   return
 }
}

上述代码中,程序阻塞在select位置等待channel的准备就绪。

当channel1的可发送时,发送字符串到channel中,当channel2可接收时,接收int值并打印。用一个select实现对多个channel的处理。


非阻塞的channel接收和发送

对channel进行接收会出现阻塞等待其他goroutine发送的情况,或者对channel进行发送会出现阻塞等待其他goroutine接收的情况。

使用select可以在本该阻塞的场景中实现非阻塞的效果。

通过定义default,让select在所有case事件都不满足的情况下不是进入阻塞状态,而是执行default的处理,来达到非阻塞的效果。

比如:

func selectFunc(channel1 chan string, channel2 chan int) {
 sendStr :="grandLine"
 select {
  case channel1 <- sendStr:
   fmt.Println("send data:",sendStr)
  case recvNum := <- channel2:
   fmt.Println("recv data:",recvNum)
   return
        default:
   fmt.Println("没有channel可以处理")
 }
}

上述代码中,当channel没有准备就绪时,程序不会阻塞在select位置等待,可是执行default分支代码。


最佳实践:for+select

比如:

func selectFunc(channel1 chan string, channel2 chan int) {
 for {
  select {
  case recvStr:= <- channel1:
   fmt.Println("recv data1:", recvStr)
  case recvNum := <-channel2:
   fmt.Println("recv data2:", recvNum)
   return
  default:
   fmt.Println("没有channel可以处理")
   time.Sleep(1 * time.Second)
  }
 }
}

如上代码:

通过for循环不断处理select对应的channel接收事件,形成了类似生产者消费者模式,不断处理其他goroutine发送的channel消息。

2.6 for range

类似于数组,切片,map可以使用for range进行遍历,channel也可以使用for range进行读取数据,一直到channel被关闭。

当channel中没有数据是会阻塞当前goroutine,这里阻塞和读channel时阻塞处理机制一样。

比如:

func rangeFunc(channel1 chan string) {
 for recvStr := range channel1 {
  fmt.Println("recv data:", recvStr)
  if len(channel1) <= 0 {
   break
  }
 }
}

三、channel原理

3.1 channel内部结构

在src/runtime/chan.go中定义了channel的内部结构:

type hchan struct {
 qcount   uint           //存储队列中元素的个数 total data in the queue
 dataqsiz uint           //存储队列的容量 size of the circular queue
 buf      unsafe.Pointer //消息数据的存储队列的指针 points to an array of dataqsiz elements
 elemsize uint16  //元素长度
 closed   uint32  //是否关闭
 elemtype *_type // 元素类型 element type
 sendx    uint   // 发送写入存储队列位置的索引 send index
 recvx    uint   // 接收读出存储队列位置的索引 receive index
 recvq    waitq  // 接收者的goroutine等待队列 list of recv waiters
 sendq    waitq  // 发送者的goroutine等待队列 list of send waiters
 // 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 //锁,存在竞争,保证并发安全
}
type waitq struct {
 first *sudog //表示等待中的 Goroutine,形成链表
 last  *sudog
}

可以看出来,channel主要由一个数据的存储队列+分别等待发送和接收的两个goroutine队列+锁构成。

本质上,channel是一个用于同步和通信的有锁队列。

实质上还是基于共享内存,但是编程范式上使用通信的方式,更直观易理解。

3.2 channel创建实现

知道了内部结构之后,我们对channel的实现也就有所猜测了。

channel的创建可以看在src/runtime/chan.go中的makechan函数。

大致代码如下:

func makechan(t *chantype, size int) *hchan {
 elem := t.elem
 ......
 mem, overflow := math.MulUintptr(elem.size, uintptr(size)) //计算buf队列需要多少空间
 ......
 c = new(hchan)   //用new函数创建hchan结构体
 c.buf = mallocgc(mem, elem, true) //初始化channel中的存储队列
 c.elemsize = uint16(elem.size)  //元素的大小(由类型决定)
 c.elemtype = elem     //元素类型
 c.dataqsiz = uint(size)   //队列长度,可存储元素个数
 ......
 return c
}

从上面代码可以看出,创建channel实际实在实例化了hchan的结构体,并返回一个*hchan指针。

所以我们在函数间参数传递时,无需特意使用指针进行传递channel,因为使用的channel本身就是一个指针。

3.3 channel发送实现

ch <- val  语句最终会被编译器解析调用src/runtime/chan.go中的chansend函数。

大致代码如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
 ......
    //加锁
 lock(&c.lock)  
    //1.判断channel是否已经关闭,关闭则抛异常
 if c.closed != 0 { 
  unlock(&c.lock)
  panic(plainError("send on closed channel"))
 }
 //2.判断接收的goroutine队列是否存在接收者在等待,
    //有则调用send直接将数据发送给阻塞等待的接收者,返回发送成功
 if sg := c.recvq.dequeue(); sg != nil {
  // Found a waiting receiver. We pass the value we want to send
  // directly to the receiver, bypassing the channel buffer (if any).
        //两个步骤,一是拷贝数据接收者对应变量的内存地址
        //二是把接收的goroutine的状态改为可运行状态放回处理器让其调度运行
  send(c, sg, ep, func() { unlock(&c.lock) }, 3)
  return true
 }
    //3.当存储队列的元素个数小于存储队列的长度时,
    //将发送的数据写入存储队列中,返回发送成功
 if c.qcount < c.dataqsiz {
  // Space is available in the channel buffer. Enqueue the element to send.
  qp := chanbuf(c, c.sendx)
  if raceenabled {
   raceacquire(qp)
   racerelease(qp)
  }
  typedmemmove(c.elemtype, qp, ep)
  c.sendx++
  if c.sendx == c.dataqsiz {
   c.sendx = 0
  }
  c.qcount++
  unlock(&c.lock)
  return true
 }
 // 是否阻塞,默认是从chansend1函数进来调用chansend。
    //所以通常block为true,需要进入后续代码,阻塞当前goroutine。
    //如果是通过select进行发送,则可能是false。
 if !block {
  unlock(&c.lock)
  return false
 }
    //4. 当接收者队列为空,存储队列满了,
    //则将自身添加到发送者队列,阻塞其他goroutine接收数据
 // Block on the channel. Some receiver will complete our operation for us.
    //获取当前goroutine
 gp := getg()
    //封装sudog,把当前goroutine添加到发送者队列
 mysg := acquireSudog()
 mysg.releasetime = 0
 if t0 != 0 {
  mysg.releasetime = -1
 }
 // No stack splits between assigning elem and enqueuing mysg
 // on gp.waiting where copystack can find it.
 mysg.elem = ep
 mysg.waitlink = nil
 mysg.g = gp
 mysg.isSelect = false
 mysg.c = c
 gp.waiting = mysg
 gp.param = nil
 c.sendq.enqueue(mysg)
    //添加到发送者队列完后,当前goroutine进行阻塞状态
 gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
 // Ensure the value being sent is kept alive until the
 // receiver copies it out. The sudog has a pointer to the
 // stack object, but sudogs aren't considered as roots of the
 // stack tracer.
 KeepAlive(ep)

 // someone woke us up.
 if mysg != gp.waiting {
  throw("G waiting list is corrupted")
 }
 gp.waiting = nil
 gp.activeStackChans = false
 if gp.param == nil {
  if c.closed == 0 {
   throw("chansend: spurious wakeup")
  }
  panic(plainError("send on closed channel"))
 }
 gp.param = nil
 if mysg.releasetime > 0 {
  blockevent(mysg.releasetime-t0, 2)
 }
 mysg.c = nil
    //被唤醒后施放当前sudog
 releaseSudog(mysg)
    //返回发送成功
 return true
}

梳理一下之后,大致流程如下(可直接看代码中的注释):

(1)判断channel是否已经关闭,关闭则抛异常;

(2)判断接收的goroutine队列是否存在接收者在等待,有则调用send直接将数据发送给阻塞等待的接收者,返回发送成功;

(3)当存储队列的元素个数小于存储队列的长度时,将发送的数据写入存储队列中,返回发送成功;

(4)当接收者队列为空,存储队列满了,则将自身添加到发送者队列,阻塞其他goroutine接收。

3.4 channel接收实现

val <- ch  语句最终会被编译器解析调用src/runtime/chan.go中的chanrecv函数。

大致逻辑和发送的处理逻辑差不多。

大致代码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
 ......
    //加锁
 lock(&c.lock) 
    //1. 判断channel已经关闭且存储队列为空,则立即返回
 if c.closed != 0 && c.qcount == 0 {
  if raceenabled {
   raceacquire(c.raceaddr())
  }
  unlock(&c.lock)
  if ep != nil {
   typedmemclr(c.elemtype, ep)
  }
  return true, false
 }
    //2. 判断发送的goroutine队列是否存在发送者在等待,
    //有则调用recv把数据拷贝到ep内存地址
 if sg := c.sendq.dequeue(); sg != nil {
  // Found a waiting sender. If buffer is size 0, receive value
  // directly from sender. Otherwise, receive from head of queue
  // and add sender's value to the tail of the queue (both map to
  // the same buffer slot because the queue is full).
        //两种情况,数据存储队列长度为0,则直接拷贝发送的数据到ep,
        //不为0则拷贝队列数据,并把发送数据拷贝到队列中。
        //然后把发送的goroutine的状态改为可运行状态放回处理器让其调度运行。
  recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
  return true, true
 }
    //3. 存储队列存在数据,则直接从recvx对应索引位置取出数据进行处理
 if c.qcount > 0 {
  // Receive directly from queue
  qp := chanbuf(c, c.recvx)
  if raceenabled {
   raceacquire(qp)
   racerelease(qp)
  }
  if ep != nil {
   typedmemmove(c.elemtype, ep, qp)
  }
  typedmemclr(c.elemtype, qp)
  c.recvx++
  if c.recvx == c.dataqsiz {
   c.recvx = 0
  }
  c.qcount--
  unlock(&c.lock)
  return true, true
 }
    //正常block是true,使用select语句时就会存在block为false的情况
 if !block {
  unlock(&c.lock)
  return false, false
 }
 //4. 和发送差不多,当发送者goroutine队列为空,存储队列也为空时,
    //则将自身添加到接收者队列,阻塞等待其他goroutine发送数据
 // no sender available: block on this channel.
 gp := getg()
 mysg := acquireSudog()
 mysg.releasetime = 0
 if t0 != 0 {
  mysg.releasetime = -1
 }
 // No stack splits between assigning elem and enqueuing mysg
 // on gp.waiting where copystack can find it.
 mysg.elem = ep
 mysg.waitlink = nil
 gp.waiting = mysg
 mysg.g = gp
 mysg.isSelect = false
 mysg.c = c
 gp.param = nil
    //把当前goroutine放到接收者队列中
 c.recvq.enqueue(mysg)
    //挂起当前goroutine进入阻塞状态
 gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

 // someone woke us up
 if mysg != gp.waiting {
  throw("G waiting list is corrupted")
 }
 gp.waiting = nil
 gp.activeStackChans = false
 if mysg.releasetime > 0 {
  blockevent(mysg.releasetime-t0, 2)
 }
 closed := gp.param == nil
 gp.param = nil
 mysg.c = nil
 releaseSudog(mysg)
 return true, !closed
}

梳理一下之后,大致流程如下(可直接看代码中的注释):

(1)判断channel已经关闭且存储队列为空,则立即返回;

(2)判断发送的goroutine队列是否存在发送者在等待,有则调用recv把数据拷贝到ep内存地址;

(3)存储队列存在数据,则直接从recvx对应索引位置取出数据进行处理;

(4)和发送差不多,当发送者goroutine队列为空,存储队列也为空时,则将自身添加到接收者队列,阻塞等待其他goroutine发送数据。

3.5 channel关闭实现

channel关闭的实现在src/runtime/chan.go中的closechan()函数中。

就不贴代码了。主要逻辑就是把接收和发送的队列对应的goroutine清空掉,让这些goroutine可以重新被调度。

四、channel和java的对比

在java中并没有channel这样的通信机制,没法对比。

java直接使用共享内存进行线程间通信。

使用channel进行并发通信,跟java直接使用共享内存通信的方式有什么好处?效率会更高吗?

不会。

channel的本质是一个有锁队列,本质还是共享内存。

同一个进程内最高效的通信方式,肯定是共享内存。

哪怕是同一个主机内多个进程间最高效的通信方式,也是共享内存,虽然不同进程会有不同的内存空间,但是可以通过操作系统提供的内存共享的方式进行共享内存。

channel基于共享内存之上做了复杂封装,其性能肯定不如直接使用共享内存的方式。

做了额外封装的好处是简单直观,不好的地方就是做了额外的工作就会有额外的消耗。

五、总结

在这篇文章中,我们了解了go并发编程重要的元素:channel。

简单了解了channel的使用,channel的原理。


go提供了channel,使用消息进行并发通信,为我们在并发编程中带来的高层的抽象。

为什么要使用channel呢?

为什么要使用消息进行并发通信呢?

有了channel后,我们的代码会更为简单直观,逻辑更为清晰。

思维发散一下,咱们类比一下经常谈到的消息队列的优点,使用channel也有逻辑解耦功能。

我们把一切逻辑分成读和写,读和写操作在我们的代码中无处不在。

而使用了channel,区别于正常读写,就产生了生产者(写)消费者(读)的直观视角。

我们不用担心读写的竞争问题,数据冲突问题,而是看到井然有序的生产和消费。


在思考go语言为什么要设计channel的同时,我又想着为什么其他语言不这样子设计?

这种设计是进步还是退步?

你觉得呢?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《Go语言学习笔记.pdf》是一本关于Go语言学习学习笔记,内容丰富且简洁明了。本书从基础知识开始,逐步介绍了Go语言的语法、特性和常用库函数等。在学习笔记中,作者通过实际的示例和练习帮助读者理解Go语言的概念和用法。 第一章介绍了Go语言的起源和发展,为读者提供了对Go语言背景的整体了解。第二章讲解了Go语言的基本语法,例如变量声明、循环和条件语句等。通过大量的代码示例,读者能够更好地理解Go语言的语法和结构。 接下来的章节重点介绍了Go语言的并发编程和高级特性。第三章详细介绍了Go语言中的goroutine和channel,这是Go语言并发编程的核心机制。作者通过生动的示例代码和实际应用案例,向读者展示了如何使用goroutine和channel实现并发编程。 第四章和第五章分别介绍了Go语言中的面向对象编程和函数式编程。通过深入讲解Go语言中的结构体、接口和函数,读者能够更好地应用这些特性进行代码设计和开发。 最后几章则介绍了Go语言中常用的库函数和工具。例如,第六章介绍了Go语言中用于网络编程的net包和http包。读者可以学习到如何使用这些库函数构建基于网络的应用程序。 总的来说,《Go语言学习笔记.pdf》是一本非常实用的Go语言学习资料。通过阅读这本书,读者能够系统地学习和理解Go语言的基本概念和高级特性,为之后的Go语言开发打下坚实的基础。无论是初学者还是有一定编程经验的开发者,都能从中获得丰富的知识和经验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值