高并发下的通信方式 channel

837 阅读8分钟

不要用共享内存的方式通信,而要用通信的方式共享内存

channelgo 中的一等公民

通过共享内存来通信,会导致数据竞争,从而导致程序出现不可预知的错误

func watch (p *int) {
  for {
    if *p == 1 {
      fmt.Println(*p)
      break
    }
  }
}
func main() {}{
  i := 0
  go watch(&i)
  time.Sleep(1 * time.Second)
  i = 1
}

通过通信的方式共享内存,可以避免数据竞争

func watch (c chan int) {
  if <-c == 1 {
    fmt.Println(*p)
  }
}
func main() {
  c := make(chan int)
  go watch(c)
  time.Sleep(time.Second)
  c <- 1
}

如何设计 Channel

channel 由三部分组成:

  • 缓存区
  • 发送等待队列
  • 接收等待队列

go-1转存失败,建议直接上传图片文件go-1.png

channelgo 中本质是一个 hchan 的结构体,定义在 runtime/chan.go

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
}

channel 的缓存区由 qcountdataqsizbufelemsizeelemtype5 个字段组成的环形缓存区(ring buffer)

  • buf:是一个指针,指向 ring buffer 的起始地址
  • elemsize:是一个 uint16 类型的字段,表示元素的大小
  • elemtype:是一个指针,指向元素的类型
  • qcount:是一个 uint 类型的字段,表示缓存区中的元素个数

go-2.png

环形缓存区的好处是可以循环使用数据,相比于队列的优点是大幅降低了 GC 的开销

发送队列由:

  • sendx:是一个 uint 类型的字段,表示发送的索引
  • sendq:是一个 waitq 类型的字段,表示发送等待队列

接收队列由:

  • recvx:是一个 uint 类型的字段,表示接收的索引
  • recvq:是一个 waitq 类型的字段,表示接收等待队列

waitq 是一个链表的结构体,定义在 runtime/chan.go 中,first 指向链表的第一个成员,last 指向链表的最后一个成员,它们都是 sudog 类型的指针

type waitq struct {
  first *sudog
  last  *sudog
}

hchan 结构体还有一个 lock 字段,是一个 mutex 类型的字段,用来保护 hchan 结构体的所有字段

所以 channel 本身不是无锁的

那为什么 channel 的并发量还是很大呢?是因为只有在存数据或者取数据的时候才会加锁,操作完之后立马就会释放锁,所以 channel 的并发量会很大

  • closed:是一个 uint32 类型的字段,表示 channel 是否关闭,0 表示开始,1 表示关闭

go-3.png

channel 发送数据的底层原理

发送数据在 go 中表示 c <- 1,它的底层是调用 runtime.chansend1 函数

chansend1 函数定义在 runtime/chan.go 中,它的作用是向 channel 中发送数据

chansend1 函数调用 chansend 函数

func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

channel 发送数据分为三种情况:

  1. 直接发送
  2. 放入缓存
  3. 休眠等待

直接发送

在数据发送之前,已经有协程在等待接收数据

这个时候缓存中是没有数据,因为如果有数据的话就不会等待了

协程会放入 recvq 队列中等待,等待数据过来被唤醒

数据过来后,直接唤醒 recvq 队列中的协程,然后将数据发送给它,不需要放入缓存区

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
   // 从 recvq 队列中取一个等待的队列
  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)
    return true
  }
}

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
  if sg.elem != nil {
    sendDirect(c.elemtype, sg, ep)
    sg.elem = nil
  }
  // 唤醒沉睡的协程
  goready(gp, skip+1)
}

func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
	dst := sg.elem
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
	// No need for cgo write barrier checks because dst is always
	// Go memory.
  // 取了一个目的地的指针 sg.elem,就是 i,i := <-c
  // 直接将要发送的数据移动给了要接收的那个变量
	memmove(dst, src, t.Size_)
}

go-4.png

放入缓存

方法缓存的意思是没有协程在等待接收数据,但是缓存区中有空间可以放数据

放入缓存的逻辑不需要和其他协程有交集,直接放入缓存区即可

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  // 缓存区最多能缓存多少
  // 从环形缓存区拿出一个可以用的缓存
  if c.qcount < c.dataqsiz {
    // Space is available in the channel buffer. Enqueue the element to send.
    qp := chanbuf(c, c.sendx)
    if raceenabled {
      racenotify(c, c.sendx, nil)
    }
    // 将要发送的数据放到可以用的缓存区中
    typedmemmove(c.elemtype, qp, ep)
    // 索引+1
    c.sendx++
    if c.sendx == c.dataqsiz {
      c.sendx = 0
    }
    // 增加缓存区中的元素个数
    c.qcount++
    unlock(&c.lock)
    return true
  }
}

go-5.png

休眠等待

休眠等待的意思是没有协程在等待接收数据,缓存区中也没有空间可以放数据

这个时候就需要把当前协程包装成 sudog 放入 sendq 队列中等待,等待有协程来接收数据

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  // 拿到自己的结构体
  gp := getg()
  // 把自己包装成 sudog
  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
  // 把自己放到 sendq 队列中
  c.sendq.enqueue(mysg)
  // Signal to anyone trying to shrink our stack that we're about
  // to park on a channel. The window between when this G's status
  // changes and when we set gp.activeStackChans is not safe for
  // stack shrinking.
  gp.parkingOnChan.Store(true)
  // 休眠
  gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
}

go-6.png

channel 接收数据的底层原理

接收数据在 go 中有两种表示方式:

  • i <-c,它的底层是调用 runtime.chanrecv1 函数
  • i, ok <-c,它的底层是调用 runtime.chanrecv2 函数

chanrecv1chanrecv2 函数最终都会调用 chanrecv 函数

func chanrecv1(c *hchan, elem unsafe.Pointer) {
  chanrecv(c, elem, true)
}

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

channel 接收数据分为四种情况

  • 有等待的协程,从协程接收
  • 有等待的协程,从缓存接收
  • 接收缓存
  • 阻塞接收

有等待的协程,从协程接收

在接收数据之前,已经有协程在等待发送数据,而且这个 channel 没有缓存

这时候直接从 send queue 中唤醒一个协程,并将数据拷贝给这个协程

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  // 从 sendq 队列中取一个等待的协程
  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
  }
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
  // 判断缓存区是否为空
  if c.dataqsiz == 0 {
    if ep != nil {
      // copy data from sender
      recvDirect(c.elemtype, sg, ep)
    }
  }
  // 唤醒休眠的协程
  goready(gp, skip+1)
}
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
  // dst is on our stack or the heap, src is on another stack.
  // The channel is locked, so src will not move during this
  // operation.
  src := sg.elem
  typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
  // 直接将要发送的数据移动给了要接收的那个变量
  memmove(dst, src, t.Size_)
}

go-7.png

有等待的协程,从缓存接收

在接收数据前,已经有协程在休眠等待,并且缓存区由缓存

从缓存区中取出一个缓存,然后从 send queue 中唤醒一个协程,并将数据拷贝给这个协程

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  // 从 sendq 队列中取一个等待的协程
  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
  }
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
  // 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)
  // 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
  goready(gp, skip+1)
}

go-8.png

接收缓存

Receive queue 中没有等待的协程,但是缓存区中有数据,直接从缓存中取出数据

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  if c.qcount > 0 {
    // Receive directly from queue
    qp := chanbuf(c, c.recvx)
    // 从缓存中取出一个数据,然后拷贝给接收者
    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
  }
}

go-9.png

阻塞接收

没有协程在休眠等待,而且缓存区中没有缓存,这时候就需要把当前协程包装成 sudog 放入 recvq 队列中等待,等待有协程来发送数据

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  // 拿到自己的结构体
  gp := getg()
  // 把自己包装成 sudog
  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
  // 把自己放到 recvq 队列中
  c.recvq.enqueue(mysg)
  // Signal to anyone trying to shrink our stack that we're about
  // to park on a channel. The window between when this G's status
  // changes and when we set gp.activeStackChans is not safe for
  // stack shrinking.
  gp.parkingOnChan.Store(true)
  // 休眠
  gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)
}

go-10.png