golang源码分析--channel

channel的概念

channel是goroutine之间的通信机制,它可以让一个goroutine通过它给另一个goroutine发送数据,每个channel在创建的时候必须指定一个类型,指定的类型是任意的。
channel 可以看成一个 FIFO 队列,对 FIFO 队列的读写都是原子的操作,不需要加锁。
对channel的操作总结:

操作 nil channel closed channel not-closed non-nil channel
close panic panic 成功 close
写 ch <- 一直阻塞 panic 阻塞或成功写入数据
读 <- ch 一直阻塞 读取对应类型零值 阻塞或成功读取数据

channel的内部实现

如图所示,在 channel 的内部实现中(具体定义在 $GOROOT/src/runtime/chan.go 里),维护了 3 个队列:

1.读等待协程队列 recvq,维护了阻塞在读此 channel 的协程列表
2.写等待协程队列 sendq,维护了阻塞在写此 channel 的协程列表
3.缓冲数据队列 buf,用环形队列实现,不带缓冲的 channel 此队列 size 则为 0

在这里插入图片描述
当协程尝试从未关闭的 channel 中读取数据时,内部的操作如下:
1.当 buf 非空时,此时 recvq 必为空,buf 弹出一个元素给读协程,读协程获得数据后继续执行,此时若 sendq 非空,则从 sendq 中弹出一个写协程转入 running 状态,待写数据入队列 buf ,此时读取操作 <- ch 未阻塞;
2.当 buf 为空但 sendq 非空时(不带缓冲的 channel),则从 sendq 中弹出一个写协程转入 running 状态,待写数据直接传递给读协程,读协程继续执行,此时读取操作 <- ch 未阻塞;
3.当 buf 为空并且 sendq 也为空时,读协程入队列 recvq 并转入 blocking 状态,当后续有其他协程往 channel 写数据时,读协程才会重新转入 running 状态,此时读取操作 <- ch 阻塞。

类似的,当协程尝试往未关闭的 channel 中写入数据时,内部的操作如下:
1.当队列 recvq 非空时,此时队列 buf 必为空,从 recvq 弹出一个读协程接收待写数据,此读协程此时结束阻塞并转入 running 状态,写协程继续执行,此时写入操作 ch <- 未阻塞;
2.当队列 recvq 为空但 buf 未满时,此时 sendq 必为空,写协程的待写数据入 buf 然后继续执行,此时写入操作 ch <- 未阻塞;
3.当队列 recvq 为空并且 buf 为满时,此时写协程入队列 sendq 并转入 blokcing 状态,当后续有其他协程从 channel 中读数据时,写协程才会重新转入 running 状态,此时写入操作 ch <- 阻塞。

当关闭 non-nil channel 时,内部的操作如下:
1.当队列 recvq 非空时,此时 buf 必为空,recvq 中的所有协程都将收到对应类型的零值然后结束阻塞状态;
2.当队列 sendq 非空时,此时 buf 必为满,sendq 中的所有协程都会产生 panic ,在 buf 中数据仍然会保留直到被其他协程读取。

channel的创建

  ch := make(chan int)
  arrch := make(chan int,1)   

channel的使用

1.简单读取和写入
读取一个已关闭的 channel 时,总是能读取到对应类型的零值,为了和读取非空未关闭 channel 的行为区别,可以使用两个接收值:

 ch <- 3
// ok is false when ch is closed
v, ok := <-ch
//普通情况
v := <-ch

2.一对一通知

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan struct{})
    nums := make([]int, 100)

    go func() {
        time.Sleep(time.Second)
        for i := 0; i < len(nums); i++ {
            nums[i] = i
        }
        // send a finish signal
        ch <- struct{}{}
    }()

    // wait for finish signal
    <-ch
    fmt.Println(nums)
}

3.广播通知
原理:利用从已关闭的 channel 读取数据时总是非阻塞的特性,可以实现在一个协程中向其他多个协程广播某个事件发生的通知

package main

import (
    "fmt"
    "time"
)

func main() {
    N := 10
    exit := make(chan struct{})
    done := make(chan struct{}, N)

    // start N worker goroutines
    for i := 0; i < N; i++ {
        go func(n int) {
            for {
                select {
                // wait for exit signal
                case <-exit:
                    fmt.Printf("worker goroutine #%d exit\n", n)
                    done <- struct{}{}
                    return
                case <-time.After(time.Second):
                    fmt.Printf("worker goroutine #%d is working...\n", n)
                }
            }
        }(i)
    }

    time.Sleep(3 * time.Second)
    // broadcast exit signal
    close(exit)
    // wait for all worker goroutines exit
    for i := 0; i < N; i++ {
        <-done
    }
    fmt.Println("main goroutine exit")
}

channel结构体

可与上文图片对照观看

/**
定义了 channel 的结构体
*/
type hchan struct {
	qcount   uint           // 队列(缓冲区)中的当前数据的个数
	dataqsiz uint           // 循环队列(缓冲区)中数据大小
	buf      unsafe.Pointer // 数据缓冲区,存放数据的环形数组
	elemsize uint16         // channel 中数据类型的大小 (单个元素的大小)
	closed   uint32         // 表示 channel 是否关闭的标识位
	elemtype *_type // element type  队列中的元素类型
    // send 和 recieve 的索引,用于实现环形数组队列
	sendx    uint   // send index    当前发送元素的索引
	recvx    uint   // receive index    当前接收元素的索引
	recvq    waitq  // list of recv waiters    接收等待队列;由 recv 行为(也就是 <-ch)阻塞在 channel 上的 goroutine 队列
	sendq    waitq  // list of send waiters    发送等待队列;由 send 行为 (也就是 ch<-) 阻塞在 channel 上的 goroutine 队列
 
	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
    // lock保护hchan中的所有字段,以及此通道上阻塞的sudoG中的几个字段。  
	//
	// 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.
    // 保持此锁定时不要更改另一个G的状态(特别是,没有准备好G),因为这可能会因堆栈收缩而死锁。
	lock mutex
}
/**
发送及接收队列的结构体
等待队列的链表实现
*/
type waitq struct {
	first *sudog
	last  *sudog
}

概括:
channel其实就是由一个环形数组实现的队列,用于存储消息元素;两个链表实现的 goroutine 等待队列,用于存储阻塞在 recv 和 send 操作上的 goroutine;一个互斥锁,用于各个属性变动的同步,只不过这个锁是一个轻量级锁。其中 recvq 是读操作阻塞在 channel 的 goroutine 列表,sendq 是写操作阻塞在 channel 的 goroutine 列表。列表的实现是 sudog,其实就是一个对 g 的结构的封装。

扫描二维码关注公众号,回复: 9355507 查看本文章

参考文章:
https://www.cnblogs.com/tobycnblogs/p/9935465.html
https://blog.csdn.net/qq_25870633/article/details/83388952

发布了212 篇原创文章 · 获赞 33 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/hello_bravo_/article/details/104105087