Go进阶之管道实现原理

37 阅读7分钟

管道是Go语言层面提供的协程间的通信方式.比unix管道更易用也更轻便.

1.初始化:

声明初始化管道的方式:

变量声明.这种方式声明管道.值为nil.并且每个管道只能存储一种类型的数据.

使用内置函数make().这种方式可以声明有缓冲通道和无缓冲通道.

示例:

	//变量声明管道.
	var ch chan int
	//make声明.
	//无缓冲管道.
	chInt := make(chan int)
	//有缓冲管道
	chString := make(chan string, 5)
}

2.操作符:

操作符"<-"表示数据流向.管道在左表示向管道写入数据.管道在右表示从管道读取数据.

示例:

    //创建一个缓冲区为10的int类型的管道.
    chanInt := make(chan int, 10)
    //向管道写入数据.
    chanInt <- 10
    //从管道读取数据.
    i := <-chanInt
    fmt.Println(i)
}

默认的管道为双向可读写.管道在函数传递时可以使用操作符限制管道读写.

示例:

    //管道可读写.
}

func ChanR(ch <-chan int) {
    //只能从管道读取数据.
}

func ChanW(ch chan<- int) {
    //只能向管道写数据.
}

数据读写:

管道没有缓冲区的时候.从管道读取数据会阻塞.直到有协程向管道中写入数据.类似的往管道中写入数据也会阻塞.直到协程从管道读取数据.

管道有缓冲区时.从管道读取数据.如果缓冲区没有数据也会阻塞.直到有协程写入数据.类似的..向管道写入数据时.如果缓冲区已满.也会阻塞.直到有协程从缓冲区中读出数据.

对于值为nil的管道.无论读写都会阻塞.而且是永久阻塞.

内置函数close()可以关闭通道.尝试向关闭的通道写入会触发panic.但关闭的通道仍可读.

管道读值示例:

    //创建一个缓冲区为10的int类型的管道.
    chanInt := make(chan int, 10)
    //向管道写入数据.
    chanInt <- 10
    chanInt <- 11
    //从管道读取数据.
    i := <-chanInt
    x, ok := <-chanInt
    fmt.Println(i)
    fmt.Println(x)
    fmt.Println(ok)
}

第一个变量表示读出的数据.第二个变量(bool)表示是否成功读取数据.

注:第二个变量不用于表示指示管道的关闭状态.

第二个变量常常会被错误的理解成管道的关闭状态.这个值确实与管道的关闭状态有关.更确切的是与管道缓冲区是否有数据有关.

关闭通道的两种情况:

情况1:管道关闭缓冲区没有数据.

情况2:管道关闭缓冲区有数据.

对于第一种情况.管道已关闭且缓冲区中没有数据.管道读取表达式返回的第一个变量为相应类型的零值.第二个变量为false.

对于第二种情况.管道已关闭且缓冲区有数据.管道读取表达式返回的第一个变量为相应的类型数据.第二个变量为true.

总结:只有管道关闭且缓冲区没有数据时.管道读取表达式的返回的第二个变量才与管道关闭状态一致.

实现原理:

数据结构:

源码位于src/runtime/chan.go:hchan.

    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
    synctest bool // true if created in a synctest bubble
    closed   uint32
    timer    *timer // timer feeding this chan
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // 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
}

qcount   uint   当前队列中剩余的元素个数.

dataqsiz uint  环形队列长度.

buf      unsafe.Pointer  环形队列指针.

elemsize uint16   每个元素的大小.

closed   uint32  标识关闭状态.

*elemtype _type 元素类型.

sendx    uint  元素写入存放队列的位置.

recvx    uint  读取元素在队列中的位置.

recvq    waitq  等待读消息的协程队列.

sendq    waitq  等待写消息的协程队列.

lock mutex  互斥锁.chan不允许并发读写.

waitq结构:

type waitq struct {
    first *sudog
    last  *sudog
}

转存失败,建议直接上传图片文件

环形队列:

chan内部实现了一个环形队列作为缓冲区.队列的长度是创建chan时指定的.

转存失败,建议直接上传图片文件​编辑

等待队列:

image.png

转存失败,建议直接上传图片文件编辑

因读阻塞的协程会被向管道写数据的协程唤醒.

写阻塞的协程会被向读数据的协程唤醒.

注:

一般不会出现读队列和写队列同时排队的情况.只有一个例外是同一个协程使用

select语句向管道一边写数据.一边读数据.此时管道会有两个队列.

类型信息:

一个管道只能传递一种类型的值.类型信息存储在hchan数据结构中.

如果需要管道存储任意类型数据.可以使用interface{}类型.

互斥锁:

一个管道同时仅允许被一个协程读写.

管道操作:

写入数据:

缓冲区有空余位置.则数据写入缓冲区.结束写入过程.

缓冲区没有空余位置.则将当前协程加入到sendq队列.进入睡眠状态等待读协程

唤醒.

image.png

转存失败,建议直接上传图片文件编辑

读取数据:

如果缓冲区有数据.则从缓冲区读取数据.结束读取过程.

缓冲区没数据.则从当前协程加入recvq队列.进入睡眠等待写协程唤醒.

转存失败,建议直接上传图片文件编辑

关闭通道:

关闭通道时会把所有recvq中的协程全部唤醒.这些协程获取的数据都为对应类型的零值.同时还会把sendq队列中的协程全部唤醒.但这些协程会触发panic.

其他panic操作:

关闭值为nil的管道.

关闭已被关闭的通道.

向已关闭的通道写入数据.

常见用法:

单向管道:

只能用于发送或接收数据.(通过上面的数据结构得知.实际没有单向管道.只是通过形参来进行限定.)

    //创建一个缓冲区为10的int类型的管道.
    chanInt := make(chan int, 10)
    readChan(chanInt)
    writeChan(chanInt)
}

func readChan(ch <-chan int) {
    i := <-ch
    fmt.Println(i)
}

func writeChan(ch chan<- int) {
    ch <- 10
}

select:

使用select可以监控多个管道.当某个管道可操作时就触发相应的case分支.

    //创建一个缓冲区为10的int类型的管道.
    ch1 := make(chan int, 10)
    ch2 := make(chan int, 10)
    go selectChan(ch1)
    go selectChan(ch2)

    for {
       select {
       case i := <-ch1:
          fmt.Println("ch1", i)
       case i2 := <-ch2:
          fmt.Println("ch2", i2)
       default:
          fmt.Println("default")
          time.Sleep(time.Second)

       }
    }

}

func selectChan(ch chan int) {
    for {
       ch <- 1
       time.Sleep(time.Second)
    }
}

for-range:

可以持续从管道中读出数据.就像遍历数组一样.当管道中没有数据时会阻塞当前

协程.与读管道阻塞处理机制是一样的.管道关闭.for-range也可以优雅的结束.

    for e := range ch {
       fmt.Println(e)
    }
}

知识就是反反复复的重复.

考虑到公众号文章过于碎片化.正在逐步把所有的知识整理到语雀知识库.后面会公开语雀地址.敬请期待.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路