典型共享内存方案

监视共享内存数据值,如果是目标值,就干某些事。

为什么使用通信来共享内存

  • 避免了协程竞争和数据冲突的问题 (相对无锁)
  • 更高级的抽象,降低开发难度,增加程序可读性 (相对有锁)
  • 模块之间更容易解耦,增加扩展性和可维护性 (相对有锁)

通道结构

 1// 源码位置:$GOPATH/src/runtime/chan.go
 2type hchan struct {
 3      //当前队列中元素的个数。当我们向channel发送数据时,qcount会增加1;当我们从channel接收数据时,qcount会减少1
 4      qcount uint
 5
 6      //如果我们在创建channel时指定了缓冲区的大小,那么dataqsiz就等于指定的大小;否则,dataqsiz为0,表示该channel没有缓冲区。
 7      dataqsiz uint
 8
 9      //buf字段是一个unsafe.Pointer类型的指针,指向缓冲区的起始地址。如果该channel没有缓冲区,则buf为nil。
10      buf unsafe.Pointer
11
12      //表示缓冲区中每个元素的大小。当我们创建channel时,Golang会根据元素的类型计算出elemsize的值。
13      elemsize uint16
14
15      // channel 是否已经关闭,当我们通过close函数关闭一个channel时,Golang会将closed字段设置为true。
16      closed uint32
17
18      //表示下一次接收元素的位置.当我们从channel接收数据时,Golang会从缓冲区中recvx索引的位置读取数据,并将recvx加1
19      recvx uint
20
21      //表示下一次发送元素的位置。在channel的发送操作中,如果缓冲区未满,则会将数据写入到sendx指向的位置,并将sendx加1。
22      //如果缓冲区已满,则发送操作会被阻塞,直到有足够的空间可用。
23      sendx uint
24
25      // 等待接收数据的 goroutine 队列,用于存储等待从channel中读取数据的goroutine。
26      //当channel中没有数据可读时,接收者goroutine会进入recvq等待队列中等待数据的到来。
27      //当发送者goroutine写入数据后,会将recvq等待队列中的接收者goroutine唤醒,并进行读取操作。
28      //在进行读取操作时,会先检查recvq等待队列是否为空,如果不为空,则会将队列中的第一个goroutine唤醒进行读取操作。
29      //同时,由于recvq等待队列是一个FIFO队列,因此等待时间最长的goroutine会排在队列的最前面,最先被唤醒进行读取操作。
30      recvq waitq
31
32      // 等待发送数据的 goroutine 队列。sendq 字段是一个指向 waitq 结构体的指针,waitq 是一个用于等待队列的结构体。
33      //waitq 中包含了一个指向等待队列中第一个协程的指针和一个指向等待队列中最后一个协程的指针。
34      //当一个协程向一个 channel 中发送数据时,如果该 channel 中没有足够的缓冲区来存储数据,那么发送操作将会被阻塞,
35      //直到有另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据。当一个协程被阻塞在发送操作时,
36      //它将会被加入到 sendq 队列中,等待另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据。
37      sendq waitq
38
39      //channel的读写锁,确保多个gorutine同时访问时的并发安全,保证读写操作的原子性和互斥性。
40      //当一个goroutine想要对channel进行读写操作时,首先需要获取lock锁。如果当前lock锁已经被其他goroutine占用,
41      //则该goroutine会被阻塞,直到lock锁被释放。一旦该goroutine获取到lock锁,就可以进行读写操作,并且在操作完成后释放lock锁,
42      //以便其他goroutine可以访问channel底层数据结构。
43      lock mutex
44}

buf 指向底层循环数组,只有缓冲型的 channel 才有。

sendq,recvq 分别表示被阻塞的 goroutine,这些 goroutine 由于尝试读取 channel 或向 channel 发送数据而被阻塞。

waitq 是 sudog 的一个双向链表,而 sudog 实际上是对 goroutine 的一个封装:

1type waitq struct {
2	first *sudog
3	last  *sudog
4}

环形缓存可以大幅度降低gc的开销

channel 并不是无锁的,操作chan是要加锁(如塞数据,塞完里面就是解锁了)。

有缓冲chan

channel 发送数据的底层原理

c<- 关键字

  • c<- 关键字是一个语法糖
  • 编译阶段,会把 c<-转为runtime.chansend1()
  • chansend1()会调用chansend()方法

channel发送的情形

  • 直接发送
    • 在发送数据前,已经有sudog在休眠等待接受 ,recvq 里面有sudog (g),此时缓存肯定是空的,不用考虑缓存 ,将数据直接拷贝给g的接受变量 (sudog.elem),唤醒G (goready)
  • 放入缓存
    • 没有g在休眠等待,但是有缓存空间
    • 将数据放入缓存
      • 获取可存入的缓存地址
      • 存入数据
      • 维护索引
  • 休眠等待
    • 没有g在休眠等待,而且没有缓存或满了
    • 自己进入发送队列 ,休眠等待 (放入sendq)
      • 把自己包装成sudog,sudog放入sendq 队列,休眠并解锁
      • 被唤醒后,数据已经被取走了

总结:

  • 编译阶段,会把<- 转为runtime.chansend1()
  • 直接发送时,将数据直接拷贝到目标变量
  • 放入缓存时,将数据放入环形缓存
  • 休眠等待时,将自己包装后放入sendq,休眠

chan接受数据底层原理

<-c 关键字是一个语法糖

编译阶段 i<-c 转为runtime.chanrecv1()

编译阶段 i,ok <-c 转为runtime.chanrecv2()

最后会调用 chanrecv()方法

chan接受的情形

  • 有等待的g,从g接受(无缓存)

    • 原理
      • 接受数据前,已经有g在休眠等待发送
      • 而且这个channel 没有缓存
      • 将数据直接从G拷贝过来,唤醒G
    • 实现
      • 判断有g在发送队列等待,进入recv()
      • 判断此channel 无缓存
      • 直接从等待的g中取走数据,唤醒g。
  • 有等待的g,从缓存接受

    • 接受数据前,已经有G在休眠等待发送,而且这个Channel 有缓存,从缓存取走一个数据,
    • 将休眠G的数据放进缓存,唤醒G(goready,写如缓存即可唤醒)
    • 实现
      • 判断有g在发送队列等待,进入recv()
      • 判断此channel 有缓存
      • 从缓存中取走一个数据
      • 将g 的数据放入缓存,唤醒G
  • 接受缓存

    • 原理: 没有g在休眠等待发送,但是缓存有内容 ,直接缓存中取走
    • 实现: 判断没有g在发送队列等待 ,判断此channel有缓存 ,从缓存取走一个数据
  • 阻塞接受

    • 判断没有g在发送队列等待
    • 判断此channel无缓存
    • 将自己包装成sudog
    • sudog放入接受等待队列,休眠
    • 唤醒时,发送的G已经把数据拷贝到位

接收端总结:

  • 编译阶段,<-c会转为 chanrecv()
  • 有等待的G,并且无缓存,从G接受 (唤醒g)
  • 有等待的G, 且有缓存时,从缓存接收 ,将休眠G的数据放进缓存,唤醒G(goready)
  • 无等待的G, 且有缓存时,从缓存接收
  • 无等待的G,并且无缓存。等到喂数据。

select 原理

  • 同时存在接收,发送、默认路径
  • 首先查看是否有可以即使执行的case
  • 没有的话,有default,走default
  • 没有default,它会将当前协程放入到所有通道的等待队列中(接收或发送队列),休眠等待

timer

timer提供一个channel,定时塞入数据。