Channel实现原理分析

本文深入探讨了Go语言中的channel实现原理,包括channel的概念、使用示例、数据结构、创建过程、接收与发送机制、关闭操作以及相关问题。通过示例代码分析了channel在并发编程中的作用,阐述了channel在goroutine间通信的关键特性,并讨论了channel关闭后的读取行为。文章还提到了并发安全性和值拷贝的本质,并引用了相关参考资料。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是channel

我们来看《Go语言编程》中的一段话

channel是Go语言在语言级别提供的goroutine间的通信方式,是一种进程内的通信方式。

通俗点儿解释就是channel可以在两个或者多个goroutine之间传递消息。在Go中,goroutine和channel是并发编程的两大基石,goroutine用来执行并发任务,channel用来在goroutine之间来传递消息。

Do not communicate by sharing memory; instead, share memory by communicating.
不要通过共享内存来通信,而要通过通信来实现共享内存。

推荐一本书《Concurrency in Go》,这本书对golang中的并发做了深入的讲解。

channel的实现

1、引入概念

首先我们来看两个例子来简单看下golang中channel是如何使用的:


package main

import (
  "fmt"
)

func goroutineA(a <-chan int) {
   
   
  for {
   
   
    select {
   
   
    case val := <-a:
      fmt.Println(val)
    }
  }
}

func main() {
   
   
  ch := make(chan int)
  go goroutineA(ch)
  ch <- 3
  ch <- 5
}

很简单的一段程序,初始化了一个非缓冲的channel,然后并发一个协程去接收channel中的数据,然后往channel中连续发送两个值,首先大家先理解一组概念,什么是非缓冲型channel和缓冲型channel?对,其实很简单,make时如果channel空间不为0,就是缓冲型的channel。

ch := make(chan int)//非缓冲型
ch := make(chan int1024)//缓冲型

如果我们将go goroutineA(ch)这行代码往下移,会发生什么?对,会报

fatal error: all goroutines are asleep - deadlock!

因为channel没有缓冲,也没有正在等待接收的goroutine,这个概念接下来我会讲到。

另外,我们会看到goroutineA的入参是a <-chan int,代表一个只能用于接收的channel,也就是单向channel。

var a <-chan int//单向channel,只用于接收,也就是从channel读取数据
var a chan<- int//单向channel,只用于发送,也就是往channel写入数据

大家一定要清楚接收和发送的概念

接收代表从channel读取数据
发送代表往channel写入数据

再看一个复杂点儿的例子

package main

import (
  "fmt"
  "os"
  "os/signal"
  "syscall"
  "time"
)

var exit = make(chan string, 1)

func main() {
   
   
  go dealSignal()
  exited := make(chan struct{
   
   }, 1)
  go channel1(exited)
  count := 0
  t := time.Tick(time.Second)
Loop:
  for {
   
   
    select {
   
   
    case <-t:
      count++
      fmt.Printf("main run %d\n", count)
    case <-exited:
      fmt.Println("main exit begin")
      break Loop
    }
  }
  fmt.Println("main exit end")
}

func dealSignal() {
   
   
  c := make(chan os.Signal, 1)
  signal.Notify(c, os.Interrupt, syscall.SIGTERM)
  go func() {
   
   
    <-c
    exit <- "shutdown"
  }()
}

func channel1(exited chan<- struct{
   
   }) {
   
   
  t := time.Tick(time.Second)
  count := 0
  for {
   
   
    select {
   
   
    case <-t:
      count++
      fmt.Printf("channel1 run %d\n", count)
    case <-exit:
      fmt.Println("channel1 exit")
      close(exited)
      return
    }
  }
}

这个例子首先并发出一个dealsign方法,用来接收关闭信号,如果接收到关闭信号后往exit channel发送一条消息,然后并发运行channel1,channel1中定了一个ticker,正常情况下channel1每秒打印第一个case语句,如果接收到exit的信号,进入第二个case,然后关闭传入的exited channel,那么main中的Loop,接收到exited关闭的信号后,打印“main exit begin”, 然后退出循环,进程成功退出。这个例子演示了channel在goroutine中起到的传递消息的作用。这个例子是为了向大家展示channel在多个goroutine之间进行通信。

2、数据结构

channel为什么会天生具备这种传递消息的特性呢,我们不禁对其底层的数据结构产生兴趣,我们来看下runtime/chan.go文件,有关channel的一切底层操作都在这个文件,我们首先来看下数据结构:

type hchan struct {
   
   
  qcount   uint           // total data in the queue;chan中的元素总数
  dataqsiz uint           // size of the circular queue;底层循环数组的size
  buf      unsafe.Pointer // points to an array of dataqsiz elements,指向底层循环数组的指针,只针对有缓冲的channel
  elemsize uint16  //chan中元素的大小
  closed   uint32  //chan是否关闭
  elemtype *_type // element type;元素类型
  sendx    uint   // send index;已发送元素在循环数组中的索引
  recvx    uint   // receive index;已接收元素在循环数组中的索引
  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
}

type waitq struct {
   
   
  first *sudog
  last  *sudog
}

创建一个底层数组容量为5,元素类型为int,那么channel的数据结构如下图所示:

在这里插入图片描述
3、创建

首先我们先来了解一下 Channel 在 Go 语言中是如何创建的,Go 语言 Channel 的创建都是由 make 关键字完成的,我们在前面介绍slice和map的创建时都介绍了使用 make 关键字初始化数据结构,那么一个问题,那么Go语言是如何实现通过make方式来创建不同的数据结构的呢?

Golang 中所有形如 make(chan int, 10) 在编译期间会先被转换成 OMAKE 类型的节点,随后的类型检查阶段在发现 make 的第一个参数是 Channel 类型时会将 OMAKE 类型的节点转换成 OMAKECHAN:


func typecheck1(n *Node, top int) (res *Node) {
   
   
    switch n.Op {
   
   
    case OMAKE:
        // ...
        switch t.Etype {
   
   
        case TCHAN:
            l = nil
            if i < len(args) {
   
   
                l = args[i]
                i++
                l = typecheck(l, ctxExpr)
                l = defaultlit(l, types.Types[TINT])
                if l.Type == nil {
   
   
                    n.Type = nil
                    return n
                }
                if !checkmake(t, "buffer", l) {
   
   
                    n.Type = nil
                    return n
                }
                n.Left = l
            } else {
   
   
                n.Left = nodintconst(0)
            }
            n.Op = OMAKECHAN
        }
    }

OMAKECHAN 类型的节点最终都会在SSA中间代码生成阶段之前被转换成makechan 或者 makechan64 的函数调用:


func walkexpr(n *Node, init *Nodes) *Node {
   
   
    switch n.Op {
   
   
    case OMAKECHAN:
        size := n.Left
        fnname := "makechan64"
        argtype := types.Types[TINT64]

        if size.Type.IsKind(TIDEAL) || maxintval[size.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
   
   
            fnname = "makechan"
            argtype = types.Types[TINT]
        }

        n = mkcall1(chanfn(fnname, 1, n.Type), n.Type, init, typename(n.Type), conv(size, argtype))
    }
}

创建channel的时候,其实底层是调用makechan方法,我们来看下源码:


func makechan(t *chantype, size int) *hchan {
   
   
  elem := t.elem

  // compiler checks this but be safe.
  if elem.size >= 1<<16 {
   
   
    throw("makechan: invalid channel element type")
  }
  if hchanSize%maxAlign != 0 || elem.align > maxAlign {
   
   
    throw("makechan: bad alignment")
  }

  mem, overfl
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值