[代码走读] - 容量可扩展的channel

1,623 阅读2分钟

chan的容量是固定的,在创建chan的时候被指定,此后就不可以再修改。在一些场景中需要容量可扩展的chan,对此收集了一些支持容量可扩展chan的开源项目:

  1. chanx:由InChannel、OutChannel和可扩展的ringbuffer组成。数据从InChannel写入,从OutChannel读取;并开启协程不断按照InChannel -> Buffer -> OutChannel的方向搬运数据。
  2. infchan:使用[]chan实现chan容量的可扩展。当chan已满,会创建一个新的chan放到[]chan中;当chan为空,会将空chan剔除出[]chan
  3. unbounded-channel:使用并发安全的链表队列实现,支持容量可扩展;但是存在读取操作一直自旋、无阻塞的情况。

chanx

chanx直接将InChannel和OutChannel暴露给用户使用,其内部通过协程不断将数据按照InChannel -> Buffer -> OutChannel的路线搬运数据。

chan1.jpg

type T interface{}

type UnboundedChan struct {
    // 进行了chan的方向封装,用户只能向In写入数据,从Out读取数据。
    In     chan<- T    // channel for write
    Out    <-chan T    // channel for read

    // 循环Buffer用于存储多余数据,作为In和Out之间的缓冲,实现容量可扩展
    // buffer只会被内部协程访问,因此没有做同步互斥处理
    buffer *RingBuffer // buffer
}

func NewUnboundedChanSize(initInCapacity, initOutCapacity, initBufCapacity int) UnboundedChan {
    in := make(chan T, initInCapacity)
    out := make(chan T, initOutCapacity)
    ch := UnboundedChan{In: in, Out: out, buffer: NewRingBuffer(initBufCapacity)}
    // 启动协程,进行In->buffer->Out数据运输
    go process(in, out, ch)

    return ch
}

func process(in, out chan T, ch UnboundedChan) {
    defer close(out)
loop:
    for {
        // 从In读取数据
        val, ok := <-in
        if !ok { // in is closed
            break loop
        }

        // 优先向Out写入数据
        select {
        case out <- val:
            continue
        default:
        }

        // Out容量已满,向buffer写入数据
        ch.buffer.Write(val)
        for !ch.buffer.IsEmpty() {
            select {
            // 优先处理InChannel防止写操作阻塞, InChannel->buffer
            case val, ok := <-in:
                if !ok { // in is closed
                    break loop
                }
                ch.buffer.Write(val)

            // Buffer->OutChannel; buffer清空时,进行buffer的缩容操作
            case out <- ch.buffer.Peek():
                ch.buffer.Pop()
                if ch.buffer.IsEmpty() && ch.buffer.size > ch.buffer.initialSize { // after burst
                    ch.buffer.Reset()
                }
            }
        }
    }

    // 当InChannel关闭时,将buffer数据放到OutChannel,直到都被消费完
    for !ch.buffer.IsEmpty() {
        out <- ch.buffer.Pop()
    }

    ch.buffer.Reset()
}

chanx将OutChannel和InChannel直接暴露给用户使用,支持InChannel的close操作。但是,尽量不要进行chanx OutChannel的close操作,容易引起panic。

infchan

infchan采用[]chan实现chan容量的可扩展性,当chan已满则申请新的chan放到[]chan里面。

chan.jpg

type InfChan struct {
  // mutex 控制并发
  mut     *sync.Mutex
  // current指向正在被写入的chan
  current chan interface{}
  // chan切片,实现chan容量的可扩展性
  chans   []chan interface{}
}

// Insert 插入数据
func (ic *InfChan) Insert(i interface{}) {
    ic.mut.Lock()
    defer ic.mut.Unlock()

    select {
    case ic.current <- i:

    default:
    // chan已满,进行扩容操作
        var ch = make(chan interface{}, internalChanSize)
        ic.chans = append(ic.chans, ch)
        ch <- i
        ic.current = ch
    }
}

// Remove 获取可读的chan
func (ic *InfChan) Remove() <-chan interface{} {
    ic.mut.Lock()
    defer ic.mut.Unlock()

    if len(ic.chans[0]) == 0 {
        if len(ic.chans) > 1 {
            // chan已空,移除chan
            ic.chans = append(ic.chans[1:])
        }
    }
    return ic.chans[0]
}

unbounded-channel

unbounded-channel采用并发安全的队列实现,写入时支持容量的可扩展性;但是其读出时采用自旋等待的方式,直到队列中有新的数据才会结束等待,因此其读出操作不会被阻塞,这点与chan不符。

unbounded-channel的非堵塞队列借鉴了Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms论文

chan3.jpg

type node struct {
    value interface{}
    next  *node
}

var noPointer = &node{value: nil, next:  nil,}

func NewUnboundedChannel() *UnboundedChannel {
    sentinel := &node{
        value: nil,
        next:  noPointer,
    }

    return &UnboundedChannel{
        head: sentinel,
        tail: sentinel,
    }
}

type UnboundedChannel struct {
    head *node
    tail *node
}

func (ch *UnboundedChannel) Enqueue(value interface{}) {
    newNode := &node{
    value: value,
        next:  noPointer,
    }

    for {
        tail := ch.tail
        next := tail.next

        if tail == ch.tail {
            if next == noPointer {
                // 采用CAS操作,将tail的next指向newNode,即newNode的入队操作
                nextPtr := (*unsafe.Pointer)(unsafe.Pointer(&ch.tail.next))
                // 第39行
                if atomic.CompareAndSwapPointer(nextPtr, unsafe.Pointer(next), unsafe.Pointer(newNode)) { 
                // 采用CAS操作,将tail指向newNode,即入队成功、更新tail
                    tailPtr := (*unsafe.Pointer)(unsafe.Pointer(&ch.tail))
                    // 第42行
                    atomic.CompareAndSwapPointer(tailPtr, unsafe.Pointer(tail), unsafe.Pointer(newNode))
                    break
                }
            } else {
                // 当并发入队时,其他协程执行了newNode入队(第39行代码),这时tail不是真实的尾部
                // 采用CAS操作,将tail指向其nextNode
                tailPtr := (*unsafe.Pointer)(unsafe.Pointer(&ch.tail))
                atomic.CompareAndSwapPointer(tailPtr, unsafe.Pointer(tail), unsafe.Pointer(next))
            }
        }
    }
}

func (ch *UnboundedChannel) Dequeue() interface{} {
    for {
        head := ch.head
        tail := ch.tail
        // head为sentinel节点,所以第一个value位于head.next
        firstElement := head.next

        if head == ch.head {
            if head == tail {
                // 队列为空,自旋等待
                if firstElement == noPointer {
                    continue
                }
                // 其他协程执行了newNode入队(第39行代码),但是tail更新失败(第42行代码) -> 需要更新尾指针
                tailPtr := (*unsafe.Pointer)(unsafe.Pointer(&ch.tail))
                atomic.CompareAndSwapPointer(tailPtr, unsafe.Pointer(tail), unsafe.Pointer(firstElement))
            } else {
                // 将head元素出队,并更新head指针
                value := firstElement.value
                headPtr := (*unsafe.Pointer)(unsafe.Pointer(&ch.head))
                if atomic.CompareAndSwapPointer(headPtr, unsafe.Pointer(head), unsafe.Pointer(firstElement)) {
                    return value
                }
            }
        }
    }
}