制坯系列-Golang专题-chan

《制坯系列-Golang专题》:chan作为协程之间通信的重要方式,是替代内存共享的最佳通信方式,本文对基本原理和关键知识点做简单介绍


chan底层数据结构

type hchan struct {
    qcount   uint           // 当前队列中剩余元素个数
    dataqsiz uint           // 环形队列长度,即可以存放的元素个数
    buf      unsafe.Pointer // 环形队列指针
    elemsize uint16         // 每个元素的大小,用于在buf中定位元素位置。
    closed   uint32         // 标识关闭状态
    elemtype *_type         // 元素类型,用于数据传递过程中的赋值
    sendx    uint           // 队列下标,指示元素写入时存放到队列中的位置
    recvx    uint           // 队列下标,指示元素从队列的该位置读出
    recvq    waitq          // 等待读消息的goroutine队列
    sendq    waitq          // 等待写消息的goroutine队列
    lock mutex              // 互斥锁,chan不允许并发读写
}

缓存区环形队列

制坯系列-Golang专题-chan_第1张图片

  • dataqsiz指示了队列长度为6,即可缓存6个元素;
  • buf指向队列的内存,队列中还剩余两个元素;
  • qcount表示队列中还有两个元素;
  • sendx指示后续写入的数据存储的位置,取值[0, 6);
  • recvx指示从该位置读取数据, 取值[0, 6);

等待队列

阻塞:

  • 如果从缓冲区为空或则没有缓冲区的channel中读取数据,当前协程会加入recvq队列
  • 如果向缓冲区已满或则没有缓冲区的channel中写入数据,当前协程会加入sendq队列

 唤醒:

  • 因读取阻塞的协程会被向channel中写入数据的协程唤醒
  • 因写入阻塞的协程会被从channel中读取数据的协程唤醒

制坯系列-Golang专题-chan_第2张图片


 chan的操作

chan的创建

golang创建chan的代码示例

var ch1 chan int               // 声明,值为nil
var ch2 = make(chan int)       // 创建,不带缓冲区
var ch3 = make(chan int, 1)    // 创建,带缓冲区

chan的底层创建:创建channel的过程实际上是初始化hchan结构

func makechan(t *chantype, size int) *hchan {
    var c *hchan
    c = new(hchan)
    c.buf = malloc(元素类型大小*size)
    c.elemsize = 元素类型大小
    c.elemtype = 元素类型
    c.dataqsiz = size
    return c
}

向channel写数据

  1. 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
  2. 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
  3. 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;

制坯系列-Golang专题-chan_第3张图片

 从channel读数据

  1. 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
  2. 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  4. 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;

制坯系列-Golang专题-chan_第4张图片

 关闭channel

关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。


channel操作中的阻塞和panic

阻塞

  • 从nil管道中读取数据
  • 从无缓冲区或则缓冲区已满的管道中读取数据
  • 向nil管道中写入数据
  • 向无缓冲区或则缓冲区已满的管道中写入数据

panic

  • 位于协程中sendq队列中的协程会在关闭channel时触发panic
  • 关闭值为nil的管道
  • 关闭已被关闭的管道
  • 向已经关闭的管道写入数据

管道的常见用法

单向channel

func readChan(chanName <-chan int) {    // 通过形参限定函数内部只能从channel中读取数据
    <- chanName
}
func writeChan(chanName chan<- int) {    // 通过形参限定函数内部只能向channel中写入数据
    chanName <- 1
}
func main() {
    var mychan = make(chan int, 10)
    writeChan(mychan)
    readChan(mychan)
}

select

使用select可以监控多channel,比如监控多个channel,当其中某一个channel有数据时,就从其读出数据。

case的选择为随机,如果所有case均阻塞则选择default,如果没有default则select阻塞

package main
import (
    "fmt"
    "time"
)
func addNumberToChan(chanName chan int) {
    for {
        chanName <- 1
        time.Sleep(1 * time.Second)
    }
}
func main() {
    var chan1 = make(chan int, 10)
    var chan2 = make(chan int, 10)
    go addNumberToChan(chan1)
    go addNumberToChan(chan2)
    for {
        select {
        case e := <- chan1 :
            fmt.Printf("Get element from chan1: %d\n", e)
        case e := <- chan2 :
            fmt.Printf("Get element from chan2: %d\n", e)
        default:
            fmt.Printf("No element in chan1 and chan2.\n")
            time.Sleep(1 * time.Second)
        }
    }
}

for-range

通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。

func chanRange(chanName chan int) {
    for e := range chanName {
        fmt.Printf("Get element from chan: %d\n", e)
    }
}

你可能感兴趣的:(#,Golang专题,golang,开发语言,后端)