Golang中Channel的分析与使用

一、什么是channel

我们来看《Go语言编程》中的一段话

channel是Go语言在语言级别提供的goroutine间的通信方式,是一种进程内的通信方式。

通俗点儿解释就是channel可以在两个或者多个goroutine之间传递消息。在Go中,goroutine和channel是并发编程的两大基石,goroutine用来执行并发任务,channel用来在goroutine之间来传递消息。

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存来通信,而要通过通信来实现共享内存。

推荐一本书《Concurrency in Go》,这本书对golang中的并发做了深入的讲解。

二、channel的实现

1、引入概念

首先我们来看两个例子来简单看下golang中channel是如何使用的:

package main

import (
    "fmt"
)

func goroutineA(a <-chan int) {
    
    
    for {
    
    
        select {
    
    
        case val := <-a:
            fmt.Println(val)
        }
    }
}

func main() {
    
    
    ch := make(chan int)
    go goroutineA(ch)
    ch <- 3
    ch <- 5
}

很简单的一段程序,初始化了一个非缓冲的channel,然后并发一个协程去接收channel中的数据,然后往channel中连续发送两个值,首先大家先理解一组概念,什么是非缓冲型channel和缓冲型channel?对,其实很简单,make时如果channel空间不为0,就是缓冲型的channel。

ch := make(chan int)//非缓冲型
ch := make(chan int1024)//缓冲型

如果我们将go goroutineA(ch)这行代码往下移,会发生什么?对,会报错

fatal error: all goroutines are asleep - deadlock!

因为channel没有缓冲,也没有正在等待接收的goroutine,这个概念接下来我会讲到。

另外,我们会看到goroutineA的入参是a <-chan int,代表一个只能用于接收的channel,也就是单向channel

var a <-chan int//单向channel,只用于接收,也就是从channel读取数据
var a chan<- int//单向channel,只用于发送,也就是往channel写入数据

大家一定要清楚接收和发送的概念

接收代表从channel读取数据

发送代表往channel写入数据

再看一个复杂点儿的例子

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

var exit = make(chan string, 1)

func main() {
    
    
    go dealSignal()
    exited := make(chan struct{
    
    }, 1)
    go channel1(exited)
    count := 0
    t := time.Tick(time.Second)
Loop:
    for {
    
    
        select {
    
    
        case <-t:
            count++
            fmt.Printf("main run %d\n", count)
        case <-exited:
            fmt.Println("main exit begin")
            break Loop
        }
    }
    fmt.Println("main exit end")
}

func dealSignal() {
    
    
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
    
    
        <-c
        exit <- "shutdown"
    }()
}

func channel1(exited chan<- struct{
    
    }) {
    
    
    t := time.Tick(time.Second)
    count := 0
    for {
    
    
        select {
    
    
        case <-t:
            count++
            fmt.Printf("channel1 run %d\n", count)
        case <-exit:
            fmt.Println("channel1 exit")
            close(exited)
            return
        }
    }
}

这个例子首先并发出一个dealsign方法,用来接收关闭信号,如果接收到关闭信号后往exit channel发送一条消息,然后并发运行channel1,channel1中定于了一个ticker,正常情况下channel1每秒打印第一个case语句,如果接收到exit的信号,进入第二个case,然后关闭传入的exited channel,那么main中的Loop,接收到exited关闭的信号后,打印“main exit begin”, 然后退出循环,进程成功退出。这个例子演示了channel在goroutine中起到的传递消息的作用。这个例子是为了向大家展示channel在多个goroutine之间进行通信。

2、数据结构

channel为什么会天生具备这种传递消息的特性呢,我们不禁对其底层的数据结构产生兴趣,我们来看下runtime/chan.go文件,有关channel的一切底层操作都在这个文件,我们首先来看下数据结构:

type hchan struct {
    
    
    qcount   uint           // total data in the queue;chan中的元素总数
    dataqsiz uint           // size of the circular queue;底层循环数组的size
    buf      unsafe.Pointer // points to an array of dataqsiz elements,指向底层循环数组的指针,只针对有缓冲的channel
    elemsize uint16 //chan中元素的大小
    closed   uint32 //chan是否关闭
    elemtype *_type // element type;元素类型
    sendx    uint   // send index;已发送元素在循环数组中的索引
    recvx    uint   // receive index;已接收元素在循环数组中的索引
    recvq    waitq  // list of recv waiters,等待接收消息的goroutine队列
    sendq    waitq  // list of send waiters,等待发送消息的goroutine队列

    // 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
}

创建一个底层数组容量为5,元素类型为int,那么channel的数据结构如下图所示:

1

3、创建

首先我们先来了解一下 Channel 在 Go 语言中是如何创建的,Go 语言 Channel 的创建都是由 make 关键字完成的,我们在前面介绍slicemap的创建时都介绍了使用 make 关键字初始化数据结构,那么一个问题,那么Go语言是如何实现通过make方式来创建不同的数据结构的呢?

Golang 中所有形如 make(chan int, 10) 在编译期间会先被转换成 OMAKE 类型的节点,随后的类型检查阶段在发现 make 的第一个参数是 Channel 类型时会将 OMAKE 类型的节点转换成 OMAKECHAN

func typecheck1(n *Node, top int) (res *Node) {
    
    
    switch n.Op {
    
    
    case OMAKE:
        // ...
        switch t.Etype {
    
    
        case TCHAN:
            l = nil
            if i < len(args) {
    
    
                l = args[i]
                i++
                l = typecheck(l, ctxExpr)
                l = defaultlit(l, types.Types[TINT])
                if l.Type == nil {
    
    
                    n.Type = nil
                    return n
                }
                if !checkmake(t, "buffer", l) {
    
    
                    n.Type = nil
                    return n
                }
                n.Left = l
            } else {
    
    
                n.Left = nodintconst(0)
            }
            n.Op = OMAKECHAN
        }
    }

OMAKECHAN 类型的节点最终都会在SSA中间代码生成阶段之前被转换成makechan 或者 makechan64 的函数调用:

func walkexpr(n *Node, init *Nodes) *Node {
    
    
    switch n.Op {
    
    
    case OMAKECHAN:
        size := n.Left
        fnname := "makechan64"
        argtype := types.Types[TINT64]

        if size.Type.IsKind(TIDEAL) || maxintval[size.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
    
    
            fnname = "makechan"
            argtype = types.Types[TINT]
        }

        n = mkcall1(chanfn(fnname, 1, n.Type), n.Type, init, typename(n.Type), conv(size, argtype))
    }
}

创建channel的时候,其实底层是调用makechan方法,我们来看下源码:

func makechan(t *chantype, size int) *hchan {
    
    
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
    
    
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
    
    
        throw("makechan: bad alignment")
    }

    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
    
    
        panic(plainError("makechan: size out of range"))
    }

    // Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
    // buf points into the same allocation, elemtype is persistent.
    // SudoG's are referenced from their owning thread so they can't be collected.
    // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
    var c *hchan
    switch {
    
    
    case mem == 0:
        // Queue or element size is zero.
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // Elements do not contain pointers.
        // Allocate hchan and buf in one call.
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // Elements contain pointers.
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)

    if debugChan {
    
    
        print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "\n")
    }
    return c
}

可以看出makechan中其实主要的代码就是一个switch,针对不同的情况

1、case mem == 0代表无缓冲型channel,只分配hchan本身结构体大小的内存

2、case elem.ptrdata==0 代表元素类型不含指针,只分配hchan本身结构体大小+元素大小*个数的内存,是连续的内存空间

3、default元素类型包括指针,两次分配内存的操作

然后将buf指向对应的地址,然后是hchan中其他变量的赋值。

4、接收

接下来我们来讲channel的接收和发送,我们使用一段程序来进行讲解

func goroutineA(a <-chan int) {
    
    
    val := <- a
    fmt.Println("G1 received data: ", val)
    return
}

func goroutineB(b <-chan int) {
    
    
    val := <- b
    fmt.Println("G2 received data: ", val)
    return
}

func main() {
    
    
    ch := make(chan int)
    go goroutineA(ch)
    go goroutineB(ch)
    ch <- 3
    time.Sleep(time.Second)
}

首先创建了一个无缓冲型的channel,然后启动两个goroutine去消费channel的数据,紧接着向channel中发送数据。我们一步一步来分析channel是如何接收和发送数据的,首先来看接收

golang中接收channel数据有两种方式

i <- ch
i, ok <- ch

这两种不同的方法经过编译器的处理都会变成 ORECV 类型的节点,但是后者会在类型检查阶段被转换成 OAS2RECV 节点,我们可以简单看一下这里转换的路线图:

2

// entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    
    
    chanrecv(c, elem, true)
}

//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    
    
    _, received = chanrecv(c, elem, true)
    return
}

两者用法不同,chanrecv2可以返回channel是否关闭,但是最终调用方法都是chanrecv,我们来看下源码:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
    
    if debugChan {
    
    
        print("chanrecv: chan=", c, "\n")
    }
    //##################step1####################
    if c == nil {
    
    
        if !block {
    
    
            return
        }
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    //##################step2####################
    if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
        c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
        atomic.Load(&c.closed) == 0 {
    
    
        return
    }

    var t0 int64
    if blockprofilerate > 0 {
    
    
        t0 = cputicks()
    }

    lock(&c.lock)

    if c.closed != 0 && c.qcount == 0 {
    
    
        if raceenabled {
    
    
            raceacquire(c.raceaddr())
        }
        unlock(&c.lock)
        if ep != nil {
    
    
            typedmemclr(c.elemtype, ep)
        }
        return true, false
    }

    if sg := c.sendq.dequeue(); sg != nil {
    
    
        // Found a waiting sender. If buffer is size 0, receive value
        // directly from sender. Otherwise, receive from head of queue
        // and add sender's value to the tail of the queue (both map to
        // the same buffer slot because the queue is full).
        recv(c, sg, ep, func() {
    
     unlock(&c.lock) }, 3)
        return true, true
    }

    if c.qcount > 0 {
    
    
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)
        if raceenabled {
    
    
            raceacquire(qp)
            racerelease(qp)
        }
        if ep != nil {
    
    
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        c.recvx++
        if c.recvx == c.dataqsiz {
    
    
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

    if !block {
    
    
        unlock(&c.lock)
        return false, false
    }

    // no sender available: block on this channel.
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
    
    
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg)
    goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)

    // someone woke us up
    if mysg != gp.waiting {
    
    
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if mysg.releasetime > 0 {
    
    
        blockevent(mysg.releasetime-t0, 2)
    }
    closed := gp.param == nil
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true, !closed
}

由于源码较多,我们逐个step进行讲解;

(1)step1

如果channel是nil:如果是非阻塞模式,直接返回(false,false);如果是阻塞模式,调用goprak挂起goroutine,会阻塞下去。

//##################step1####################
    if c == nil {
    
    
        if !block {
    
    
            return
        }
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

(2)step2

快速操作(不用获取锁,快速返回),三组条件全部满足,快速返回(false,false)

条件1:首先是在非阻塞模式下

条件2:如果是非缓冲型(datasiz=0)并且等待发送goroutine队列为空(sendq.first=nil,就是没人往channel写数据),或者缓冲型channel(datasiz>0)并且buf中没有数据;

条件3:channel未关闭

//##################step2####################
    if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
        c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
        atomic.Load(&c.closed) == 0 {
    
    
        return
    }

(3)step3

首先加锁,如果channel已经关闭,并且buf中没有元素,返回对应类型的0值,但是received为false;两种情况

情形1:非缓冲型,channel已关闭

情形2:缓冲型,channel已关闭,并且buf无元素

//##################step3####################
    lock(&c.lock)

    if c.closed != 0 && c.qcount == 0 {
    
    
        if raceenabled {
    
    
            raceacquire(c.raceaddr())
        }
        unlock(&c.lock)
        if ep != nil {
    
    
            typedmemclr(c.elemtype, ep)
        }
        return true, false
    }

(4)step4

如果等待发送队列中有元素,证明channel已经满了,两种情形

情形1:非缓冲型,无buf

情形2:缓冲型,buf满了

//##################step4####################
if sg := c.sendq.dequeue(); sg != nil {
    
    
        recv(c, sg, ep, func() {
    
     unlock(&c.lock) }, 3)
        return true, true
    }

两种情形都正常进入recv方法,我们来看下源码:

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    
    
  //##################step4-1####################
    if c.dataqsiz == 0 {
    
    
        if raceenabled {
    
    
            racesync(c, sg)
        }
        if ep != nil {
    
    
            // copy data from sender
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
    
    
     //##################step4-2####################
        // Queue is full. Take the item at the
        // head of the queue. Make the sender enqueue
        // its item at the tail of the queue. Since the
        // queue is full, those are both the same slot.
        qp := chanbuf(c, c.recvx)
        if raceenabled {
    
    
            raceacquire(qp)
            racerelease(qp)
            raceacquireg(sg.g, qp)
            racereleaseg(sg.g, qp)
        }
        // copy data from queue to receiver
        if ep != nil {
    
    
            typedmemmove(c.elemtype, ep, qp)
        }
        // copy data from sender to queue
        typedmemmove(c.elemtype, qp, sg.elem)
        c.recvx++
        if c.recvx == c.dataqsiz {
    
    
            c.recvx = 0
        }
        c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
    }
    sg.elem = nil
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
    
    
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

step4-1:如果是非缓冲型,那么直接从发送者的栈copy到接收者的栈

step4-2:缓冲型的,但是buf已经满了,此时recvx和sendx是重合的,如下图

3

首先将recvx处的元素0拷贝到接收地址,然后将下一个元素5拷贝到sendx,然后recvx和sendx分别加1。

Step4-3:然后唤醒等待发送队列中的goroutine,等待调度器调度。

(5)step5

没有等待发送的队列,并且buf中有元素,直接把接收游标处的数据copy到接收数据的地址,然后改变hchan中元素数据。

if c.qcount > 0 {
    
    
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)
        if raceenabled {
    
    
            raceacquire(qp)
            racerelease(qp)
        }
        if ep != nil {
    
    
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        c.recvx++
        if c.recvx == c.dataqsiz {
    
    
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

(6)step6

如果是非阻塞,那么直接返回;如果是阻塞的,构造sudog,保存各种值;将sudog保存到channel的recvq中,调用goparkunlock将goroutine挂起

if !block {
    
    
        unlock(&c.lock)
        return false, false
    }
// no sender available: block on this channel.
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
    
    
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg)
    goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)

    // someone woke us up
    if mysg != gp.waiting {
    
    
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if mysg.releasetime > 0 {
    
    
        blockevent(mysg.releasetime-t0, 2)
    }
    closed := gp.param == nil
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true, !closed

我们用本节一开始的例子来讲解下,再贴一遍程序

func goroutineA(a <-chan int) {
    
    
    val := <- a
    fmt.Println("G1 received data: ", val)
    return
}

func goroutineB(b <-chan int) {
    
    
    val := <- b
    fmt.Println("G2 received data: ", val)
    return
}

func main() {
    
    
    ch := make(chan int)
    go goroutineA(ch)
    go goroutineB(ch)
    ch <- 3
    time.Sleep(time.Second)
}

由于我们创建的channel是无缓冲型的,所以两个goroutine启动的G1和G2会被阻塞,G1和G2被加入到recvq中,状态为waiting,等待被唤醒。此时此刻ch如下图:

4

问题:当一个channel关闭后,我们是否还能从中读出数据?

package main

import "fmt"

func main() {
    
    
    ch := make(chan int, 6)

    ch <- 1
    ch <- 2

    close(ch)

    a := <-ch
    fmt.Println(a)

    b := <-ch
    fmt.Println(b)

    c := <-ch
    fmt.Println(c)
}

输出会是什么?

1
2
0

我们可以看出,当一个channel关闭后,我们依然可以从中读出数据,如果chan的buf中有元素,则读出的是chan中buf的数据,如果buf为空,则输出对应元素类型的零值。那么我们来看下如下的一段程序:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

var exit1 = make(chan struct{
    
    }, 1)

func main() {
    
    
    go dealSignal1()
    count := 0
    t := time.Tick(time.Second)
    for {
    
    
        select {
    
    
        case <-t:
            count++
            fmt.Printf("main run %d\n", count)
        case <-exit1:
            fmt.Println("main exit begin")
        }
    }
    fmt.Println("main exit over")
}

func dealSignal1() {
    
    
    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
    
    
        <-c
        close(exit1)
    }()
}

上面这段程序会有什么问题?

5、发送

我们继续往下走,G1、G2被挂起后,往channel中发送一个数据3,其实调用的是chansend方法,我们还是逐步的去讲解

(1)step1

如果channel=nil,当前goroutine会被挂起

if c == nil {
    
    
        if !block {
    
    
            return false
        }
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

(2)step2

依然是一个不加锁的快速操作,三组条件

条件1:非阻塞

条件2:channel未关闭

条件3:channel是非缓冲型,并且等待接收队列为空;或者缓冲型,并且循环数组已经满了

if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
        (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
    
    
        return false
    }

(3)step3

加锁,如果channel已经关闭,直接panic

    lock(&c.lock)

    if c.closed != 0 {
    
    
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

(4)step4

如果等待接收队列不为空,说明什么?

情形1:非缓冲型,等待接收队列不为空

情形2:缓冲型,等待接收队列不为空(说明buf为空)

两种情形,都是直接将待发送数据直接copy到接收处

if sg := c.recvq.dequeue(); sg != nil {
    
    
        // Found a waiting receiver. We pass the value we want to send
        // directly to the receiver, bypassing the channel buffer (if any).
        send(c, sg, ep, func() {
    
     unlock(&c.lock) }, 3)//直接从ep copy到sg
        return true
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    
    
    if raceenabled {
    
    
        if c.dataqsiz == 0 {
    
    
            racesync(c, sg)
        } else {
    
    
            // Pretend we go through the buffer, even though
            // we copy directly. Note that we need to increment
            // the head/tail locations only when raceenabled.
            qp := chanbuf(c, c.recvx)
            raceacquire(qp)
            racerelease(qp)
            raceacquireg(sg.g, qp)
            racereleaseg(sg.g, qp)
            c.recvx++
            if c.recvx == c.dataqsiz {
    
    
                c.recvx = 0
            }
            c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
        }
    }
    if sg.elem != nil {
    
    
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
    
    
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

两种情形,都直接从一个用一个goroutine操作另一个goroutine的栈,因此在sendDirect方法中会有一次写屏障

func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
    
    
    // src is on our stack, dst is a slot on another stack.

    // Once we read sg.elem out of sg, it will no longer
    // be updated if the destination's stack gets copied (shrunk).
    // So make sure that no preemption points can happen between read & use.
    dst := sg.elem
    typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
    // No need for cgo write barrier checks because dst is always
    // Go memory.
    memmove(dst, src, t.size)
}

(5)step5

如果等待队列为空,并且缓冲区未满,肯定是缓冲型的channel

if c.qcount < c.dataqsiz {
    
    
        // Space is available in the channel buffer. Enqueue the element to send.
        qp := chanbuf(c, c.sendx)
        if raceenabled {
    
    
            raceacquire(qp)
            racerelease(qp)
        }
        typedmemmove(c.elemtype, qp, ep)
        c.sendx++
        if c.sendx == c.dataqsiz {
    
    
            c.sendx = 0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }

将元素放在sendx处,然后sendx加1,channel总量加1

(6)step6

如果以上情况都没有命中,说明什么?说明channel已经满了,如果是非阻塞的直接返回,否则需要调用gopack将这个goroutine挂起,等待被唤醒。

if !block {
    
    
        unlock(&c.lock)
        return false
    }

    // Block on the channel. Some receiver will complete our operation for us.
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
    
    
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    c.sendq.enqueue(mysg)
    goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
    // Ensure the value being sent is kept alive until the
    // receiver copies it out. The sudog has a pointer to the
    // stack object, but sudogs aren't considered as roots of the
    // stack tracer.
    KeepAlive(ep)

我们对照程序分析下,在前一个小节G1、G2被挂起来了,等待sender的解救;这时候往ch中发送了一个3,(step4)这时sender发现ch的等待接收队列recvq中有receiver,就会出队一个sudog,然后将元素直接copy到sudog的elem处,然后调用goready将G1唤醒,继续执行G1原来的代码,打印出结果。如下图:

5

6、关闭

close一个channel会调用closechan方法,比较简单,我们也来看下

(1)step1

如果channel为nil,会直接panic

if c == nil {
    
    
        panic(plainError("close of nil channel"))
    }

(2)step2

加锁,如果channel已经关闭,再次关闭会panic

lock(&c.lock)
    if c.closed != 0 {
    
    
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

(3)step3

首选将hchan对应close标志置为1,然后声明一个链表;将等待接收队列中的所有sudog加入到链表,并将其elem赋予一个相应类型的0值;

c.closed = 1

    var glist gList

    // release all readers
    for {
    
    
        sg := c.recvq.dequeue()
        if sg == nil {
    
    
            break
        }
        if sg.elem != nil {
    
    
            typedmemclr(c.elemtype, sg.elem)
            sg.elem = nil
        }
        if sg.releasetime != 0 {
    
    
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = nil
        if raceenabled {
    
    
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }

(4)step4

将所有等待发送队列的sudog加入链表

// release all writers (they will panic)
    for {
    
    
        sg := c.sendq.dequeue()
        if sg == nil {
    
    
            break
        }
        sg.elem = nil
        if sg.releasetime != 0 {
    
    
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = nil
        if raceenabled {
    
    
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }
    unlock(&c.lock)

(5)step5

唤醒sudog所有goroutine

for !glist.empty() {
    
    
        gp := glist.pop()
        gp.schedlink = 0
        goready(gp, 3)
    }

三、问题

1、问题1:哪些操作会使channel发生panic?

三种情况

情况1:往一个已经关闭的channel写数据

情况2:关闭一个nil的channel

情况3:关闭一个已经关闭的channel

2、问题2:channel是并发安全的吗?

3、问题3:当一个channel关闭后,我们是否还能从channel读到数据?

可以,只不过接收的是无效数据

4、问题4:channel发送和接收元素的本质是什么?

值的拷贝

看一段示例

package main
import (
    "fmt"
    "time"
)

func print(u <-chan int) {
    
    
    time.Sleep(2 * time.Second)
    fmt.Println("print int", <-u)
}

func main() {
    
    
    c := make(chan int, 5)
    a := 0

    c <- a
    fmt.Println(a)
    // modify g
    a = 1

    go print(c)
    time.Sleep(5 * time.Second)
    fmt.Println(a)
}

再看一段复杂一点的

package main

import (
    "fmt"
    "time"
)

type people struct {
    
    
    name string
}

var u = people{
    
    name: "A"}

func printPeople(u <-chan *people) {
    
    
    time.Sleep(2 * time.Second)
    fmt.Println("printPeople", <-u)
}

func main() {
    
    
    c := make(chan *people, 5)
    var a = &u

    c <- a
    fmt.Println(a)
    // modify g
    a = &people{
    
    name:"B"}

    go printPeople(c)
    time.Sleep(5 * time.Second)
    fmt.Println(a)
}

输出会是什么

&{
    
    A}
printPeople &{
    
    A}
&{
    
    B}

因为chan中保存的是u的地址的值的拷贝,这个地址未发生改变,虽然调用a = &people{name:“B”}重新赋予了a新的地址,但是channel中的未改变。

四、参考文献

【1】《深度解密Go语言之channel》

【2】《浅谈Go语言编译原理》

【3】《Concurrency in Go》

猜你喜欢

转载自blog.csdn.net/zhw21w/article/details/129488115