1

Channel is a communication method between goroutines provided by Golang, which allows one goroutine to send a specific value to another goroutine.

Features

When the channel does not have a buffer, or there is a buffer but there is no data in the buffer, reading data from the channel will block until a coroutine writes data to the channel. Similarly, when the channel does not have a buffer, or when the buffer is full, writing data to the channel will also block until a coroutine reads data from the channel. For a channel with a value of nil, both read and write will be blocked, and it is permanently blocked.

Use the built-in function close to close the channel. Trying to send data to the closed channel will trigger the panic, but it is still readable at this time. The expression read by the channel has at most two return values:

x, ok := <-ch

The first variable indicates the data read, and the second variable indicates whether the data is successfully read. Its value is only related to whether there is data in the channel buffer, and has nothing to do with the closed state of the channel.

Implementation Principle

data structure

The source code src/runtime/chan.go:hchan defines the data structure of the channel:

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

It can be seen that the channel is composed of queues, type information, and goroutine waiting queues.

circular queue

The channel implements a circular queue as its buffer, and the length of the queue is specified when the channel is created. The following figure shows a schematic diagram of a channel that can cache 6 elements:

image.png

  • dataqsiz indicates that the queue length is 6, which can cache 6 elements;
  • buf points to the memory address of the queue;
  • qcount means there are two more elements in the queue;
  • sendx represents the storage location of the subsequently written data, the value is [0, 6);
  • recvx represents the location of the read data, the value is [0, 6).
Type information

A channel can only pass one type of value:

  • elemtype represents the type, used for assignment in the data transfer process;
  • elemsize represents the type size, used to locate the element position in buf.
waiting queue

When reading data from the channel, if there is no buffer or the buffer is empty, the current coroutine will be blocked and added to the recvq queue. When writing data to the channel, if there is no buffer or the buffer is full, the current coroutine will also be blocked, and then added to the sendq queue. The coroutines in the waiting queue will be awakened when other coroutines operate the channel.

The following figure shows a channel without a buffer, and several coroutines are blocking waiting to read data:

image.png

Related operations

Create channel

The process of creating a channel is actually to initialize the hchan structure. The type information and buffer length are passed in by the make statement, and the size of buf is determined by the element size and buffer length.

The source code src/runtime/chan.go defines the function makechan() to create a channel. The simplified version of the code is as follows:

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))
        c.buf = c.raceaddr()
    case elem.ptrdata == 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
}
Send data

The operation of sending data is finally transformed into the chansend() function. The main code and logic are as follows:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 如果通道为 nil,非阻塞式发送的话直接返回 false,否则将当前协程挂起
    if c == nil {
        if !block {
            return false
        }
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }
 
    // 对于非阻塞式发送,如果通道未关闭且没有缓冲空间的话,直接返回 false
    if !block && c.closed == 0 && full(c) {
        return false
    }

    // 加锁,并发安全
    lock(&c.lock)

    // 如果通道关闭了,直接 panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

    // 如果接收队列不为空,直接将要发送的数据发送到队首的 goroutine
    if sg := c.recvq.dequeue(); sg != nil {
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }

    // 对于缓冲区还有空闲的 channel,拷贝数据到缓冲区,维护相关信息
    if c.qcount < c.dataqsiz {
        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
    }

    // 没有缓冲空间时,发送方会挂起,并根据当前 goroutine 构造一个 sudog 结构体添加到 sendq 队列中
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }

    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)

    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)

    // 省略被唤醒时部分代码

    return true
}
Read data

The operation of reading data is finally transformed into the chanrecv() function, and the main logic is as follows:

// selected 和 received 返回值分别代表是否可被 select 语句命中以及是否读取到了数据
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 如果 channel 为 nil,非阻塞式读取直接返回,否则直接挂起
    if c == nil {
        if !block {
            return
        }
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    // 非阻塞模式并且没有消息可读(没有缓冲区或者缓冲区为空),如果 channel 未关闭直接返回
    if !block && empty(c) {
        if atomic.Load(&c.closed) == 0 {
            return
        }

        if empty(c) {
            if raceenabled {
                raceacquire(c.raceaddr())
            }
            if ep != nil {
                typedmemclr(c.elemtype, ep)
            }
            return true, false
        }
    }

    // 加锁
    lock(&c.lock)

    // channel 已关闭并且没有消息可读(没有缓冲区或者缓冲区为空),会接收到零值,typedmemclr 会根据类型清理相应地址的内存
    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
    }

    // 等待发送队列不为空,如果是非缓冲型 channel,直接拷贝发送者的数据,否则接收队首的数据,并将发送者的数据移动到环形队列尾部
    if sg := c.sendq.dequeue(); sg != nil {
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true
    }

    // 缓冲型 channel,buf 里有元素,可以正常接收
    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
    }

    // 被阻塞的情况,构造一个 sudog 结构体,保存到 channel 的等待接收队列,并将当前 goroutine 挂起
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }

    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)

    atomic.Store8(&gp.parkingOnChan, 1)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // 省略被唤醒时部分代码
    
    return true, !closed
}
Close the channel

Close a channel, and finally execute the function closechan(), the core code is as follows:

func closechan(c *hchan) {
    // 如果 channel 为 nil,直接 panic
    if c == nil {
        panic(plainError("close of nil channel"))
    }

    // 加锁,如果 channel 已关闭,直接 panic
    lock(&c.lock)
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

    c.closed = 1

    var glist gList

    // 释放等待接收队列中,向需要返回值的接收者返回相应的零值
    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)
    }

    // 释放等待发送队列,相关的 goroutine 会触发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)

    // ...
}

Common applications

Timed task

This usage needs to be combined with the timer, which is divided into two types: timeout control and timing execution.

If you need to perform an operation, but don't want it to take too long, and want to give it a timeout limit, you can do this:

select {
    case <-time.After(100 * time.Millisecond):
    case <-s.stopc:
        return false
}

After waiting for 100 ms, if s.stopc has not read data or is closed, it ends directly.

It is also relatively simple to execute a certain task on a regular basis, for example, every 1 second, execute a regular task:

func worker() {
    ticker := time.Tick(1 * time.Second)
    for {
        select {
        case <- ticker:
            // 执行任务
        }
    }
}

Decoupling producers and consumers

Use a channel to save tasks, start n goroutines as a pool of work coroutines, these coroutines work in an infinite loop, read tasks from the channel and execute:

func main() {
    taskCh := make(chan int, 100)
    go worker(taskCh)

    for i := 0; i < 10; i++ {
        taskCh <- i
    }

    select {
    case <-time.After(time.Hour):
    }
}
func worker(taskCh <-chan int) {
    const N = 5

    for i := 0; i < N; i++ {
        go func(id int) {
            for {
                task := <- taskCh
                fmt.Printf("finish task: %d by worker %d\n", task, id)
                time.Sleep(time.Second)
            }
        }(i)
    }
}

control the number of concurrent

Sometimes hundreds of tasks need to be executed regularly, but the number of concurrency cannot be too high. At this time, the number of concurrency can be controlled through the channel. For example, the following example:

var limit = make(chan int, 3)

func main() {
    // …………
    for _, w := range work {
        go func() {
            limit <- 1
            w()
            <-limit
        }()
    }
    // …………
}

Construct a channel with a capacity of 3, traverse the task list, and start a goroutine for each task. The actual execution of the task is done in w(). Before executing w(), you must first get the "license" from the limit. After you get the license, you can execute w(), and you must return the "license" after performing the task. It should be noted that if w() panic occurs, the "license" may not go back yet, so you need to use defer to ensure it.


与昊
225 声望636 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道