设为首页 加入收藏

TOP

golang channel 源码剖析(二)
2019-05-23 14:35:57 】 浏览:338
Tags:golang channel 源码 剖析
个发送的地址,recvx 表示下一个接收的地址。
  • recvq 表示等待接收的 sudog 列表,一个接收语句执行时,如果缓冲区没有数据而且当前没有别的发送者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 对象放到 recvq 中。
  • sendq 类似于 recvq,一个发送语句执行时,如果缓冲区已经满了,而且没有接收者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 放到 sendq 中。
  • closed 表示通道是否已经被关闭,0 代表没有被关闭,非 0 值代表已经被关闭。
  • lock 用于对 hchan 加锁
  • type hchan struct {
        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
        closed   uint32
        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
    }
    
    type waitq struct {
        first *sudog
        last  *sudog
    }

    4. 创建通道

    当你在代码里面写了一句 c := make(chan int, 8) 时,编译器就会把它翻译成

    t := typeof(chan int) // 编译器给你生成了 chan int 的类型描述信息,然后 t 指向这个类型描述信息
    c := makechan(t, 8)

    没错,makechan 就是创建通道的入口。它的目的就是构建 hchan 对象并返回。由于 hchan 在程序中始终以引用的形式存在,通过赋值或者是传参,它指向的都是同一个对象,所以 hchan 在标准库中都是以指针形式呈现给外部的。对于 makechan 的逻辑,这里分 3 种情况:

    1. 缓冲区所需大小为 0。对于这种情况,在为 hchan 分配内存时,只需要分配 sizeof(hchan) 大小的内存。这很好理解。
    2. 缓冲区所需大小不为 0,而且数据类型不包含指针。
      我们先来理解下 不包含指针 这个东西,对于指针类型或者成员中有指针的类型,那就是包含指针的,否则就是不包含指针的。如下代码,A{}是不包含指针的,&A{}、B{}、&B{} 是包含指针的。
    type A struct {
        a int
        b int
    }
    
    type B struct {
        a *int
        b *int
    }

    对于不包含指针的这种情况,分配一块连续内存容纳 hchan 和缓冲区对象。

    1. 缓冲区所需大小不为 0,而且数据类型包含指针。对于这种情况,分配两块内存,其中一块表示 hchan 对象,另外一块用来表示 buf。

    下面是 makechan 的核心代码:

    
    func makechan(t *chantype, size int) *hchan {
        // ...
    
        mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    
        var c *hchan
        switch {
        case mem == 0:
            c = (*hchan)(mallocgc(hchanSize, nil, true))
        case elem.kind&kindNoPointers != 0:
            c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
            c.buf = add(unsafe.Pointer(c), hchanSize)
        default:
            c = new(hchan)
            c.buf = mallocgc(mem, elem, true)
        }
    
        c.elemsize = uint16(elem.size)
        c.elemtype = elem
        c.dataqsiz = uint(size)
    
        // ...
    
        return c
    }

    至于为什么要区分包含指针和不包含指针这两种情况,makechan 的注释给出了一段解释:

    Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.

    下面是我的猜想,如果不对,欢迎高人指正:

    GC 不会知道 unsafe.Pointer 里面存储的是什么类型,因此如果实际元素类型里面包含指针,就要通过 mallocgc 将分配什么类型的数据告诉 gc,这样 gc 就不会回收这块内存中存储的指针所指向的内存。反之, buf 不包含指针,可以用一块大的内存来存储 hchan 对象和缓冲区,这样可以减轻 gc 压力。

    5. 发送数据

    向通道发送数据,runtime 中通过 chansend 实现,它的声明如下:

    func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool 

    参数 c 表示要向哪个 chan 发送数据, ep 表示要发送的数据的地址,block 表示是否需要阻塞, callerpc 表示调用地址。返回值 bool 表示数据是否成功发送。

    block 是为了实现如下代码的语义:

        c := make(chan int)
            // ...
        select {
        case <-c:
            // ...
        default:
            // ...
        }

    上面这段代码被编译成对 selectnbsend 的调用:

    if selectnbsend(c, v) {
        ... foo
    } else {
        ... bar
    }

    selectnbsend 的实现如下

    func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
        return chansend(c, elem, false, getcallerpc()) // 非阻塞的发送
    }

    它与拥有多个 case 的 select 不同(多个 case 的 select 将在后文分析)。

    chansend 按照下面的逻辑执行:

    1. 如果通道是空的,对于非阻塞的发送,直接返回 false。对于阻塞的通道,将 goroutine 挂起,并且永远不会返回
        if c == nil {
            if !block {
                return false
            }
            gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
            throw("unreachable")
        }
    1. 非阻塞的情况下,如果通道没有关闭,而且当前没有接收者,缓冲区也已经满了或者没有缓冲区(即不可以发送数据)。那么直接返回 false
    if !block && c.closed ==
    首页 上一页 1 2 3 4 5 6 7 下一页 尾页 2/7/7
    】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
    上一篇Golang实现requests库 下一篇Golang websocket推送

    最新文章

    热门文章

    Hot 文章

    Python

    C 语言

    C++基础

    大数据基础

    linux编程基础

    C/C++面试题目