美文网首页
golang channel详解

golang channel详解

作者: 哈哈_dfde | 来源:发表于2020-09-07 13:49 被阅读0次

    channel是什么?

      golang语言中,channel是一个协程安全的FIFO的队列,读取和写入操作都是原子操作
    

    使用场景

      用来做多协程之间的通信,java中的线程之间的通信是通过共享内存实现的,A线程获取内存区域,并且“锁”住内存这块区域,然后执行临界区代码,这一时刻无法获取锁的其他线程阻塞,直到A线程释放锁(实际上就是释放刚才锁住的内存区域),其他线程继续竞争共享内存,获取锁的执行临界区代码。这个过程本质上是通过共享内存的方式实现多线程通信。而golang提出了新的通信方式:用通信来共享内存,而不要用共享内存来通信
    

    使用方式
    ##无缓冲区的channel
    创建 var NoRoutChannel chan 【类型】= make(chan 【类型】)
    使用场景
    只是作为信号的channel,告诉另一个协程,这件事我做完了,而不需要给另外的协程序发送做完后的通知内容
    ##有缓冲区的channel
    创建 var NoRoutChannel chan 【类型】= make(chan 【类型】,【缓冲大小】)
    使用场景
    告诉另一个协程,这件事我做完了,并且发送和另一个协程序传递的变量
    PS:注意channel读取和写入操作必须在两个不同的协程中进行,否则panic
    channel状态
    channel分为nil、open、closed

    image.png
    对于nil的channel无论读写都panic,对于closed状态的channel,向里边push的操作会报panic
    demo: 使用channel实现一个生产者消费者模式

    var wg sync.WaitGroup = sync.WaitGroup{}
    type Product struct {
    Queue chan string
    }
    func (product *Product) product(apple string){
    fmt.Println("product: "+apple)
    product.Queue <- apple
    }
    type Consumer struct {
    Queue chan string
    }
    func (consumer *Consumer) Consume(){
    con := <- consumer.Queue
    fmt.Println("consume:"+con)
    defer wg.Done()
    }
    func main() {
    testChan := make(chan string,1)
    p := Product{
    Queue: testChan,
    }
    c := Consumer{
    Queue: testChan,
    }
    go c.Consume()
    go p.product("3333")
    wg.Add(1)
    wg.Wait()
    fmt.Println("end job")
    }

    channel内部原理

    数据结构

    type hchan struct {
    qcount   uint           // 所有在队列中数据数量
    dataqsiz uint           // 环形队列大小,可以存放的元素个数
    buf      unsafe.Pointer // 只想环形队列的指针
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // 生产下标
    recvx    uint   // 消费下标
    recvq    waitq  // 消费者队列
    sendq    waitq  // 生产者队列
    
    // 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
    }
    

    channel缓冲区实现--环形队列

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

    下图展示了一个可缓存6个元素的channel示意图:

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

    等待队列

    从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
    向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。

    被阻塞的goroutine将会挂在channel的等待队列中:

    • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
    • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;

    下图展示了一个没有缓冲区的channel,有几个goroutine阻塞等待读数据:

    image

    注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据。

    make(chan string,2)分析

    代码:

    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写数据

    向一个channel中写数据简单过程如下:

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

    简单流程图如下:


    image

    从channel中读取数据

    从一个channel读数据简单过程如下:

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

    简单流程图如下:


    image

    关闭channel

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

    除此之外,panic出现的常见场景还有:

    关闭值为nil的channel
    关闭已经被关闭的channel
    向已经关闭的channel写数据

    相关文章

      网友评论

          本文标题:golang channel详解

          本文链接:https://www.haomeiwen.com/subject/euiqektx.html