golang提倡使用通信的方式来共享内存,而不是通过共享内存来通信。下面介绍channel是如何实这一思想的,以及使用channel时需要注意的地方。
一、基本原理
1.写入数据时
若channel recvq(读阻塞队列)上有被gopark的g,则直接把数据拷贝到队首g的栈空间,并唤醒g;若recvq中没有gopark的g,但缓冲区未满,则把数据放入缓冲区。
2.读取数据时
跟写数据有点不一样,当channel sendq(写阻塞队列)上有被gopark的g时分两种情况:若缓冲区大小为0则直接从g上读取数据;若缓冲区大小不为0,此时缓冲区必然已满(若不未满g也不会被gopark),则从缓冲区头部读取数据,把send g 的数据拷贝到缓冲区尾部,并goready g。
- 阻塞方式:若缓冲区中没有数据,则当前g被gopark。
- 非阻塞方式:若缓冲区中没有数据,则直接返回。
二、定义
channel可以简单的看成一个带锁的环形队列,结构体定义如下:
1 | //go version 1.17.1, 源文件:src\runtime\chan.go |
三、往Channel中写数据
1.阻塞与非阻塞
往channel写入数据分为阻塞模式和非阻塞模式:阻塞模式时若channel为nil或channel缓冲区满,当前g会被gopark。非阻塞模式遇到这两种情况则直接返回fase。
1.1阻塞
当以如下方式向channel中写入数据时,会被编译器翻译成阻塞模式,block为true
1 | select { |
1.2非阻塞
若在case后加上default则会翻译成非阻塞模式,block为false
1 | select { |
2.源码
1 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { |
四、从Channel中读数据
与channel中写数据类似,从channel中读也分为阻塞和非阻塞方式。若以阻塞方式读时,当channel为nil、channel缓冲区为空时当前g被gopark;若以非阻塞方式读时,遇到这两种情况直接返回。
1.阻塞与非阻塞
1.1阻塞
当以如下方式从channel中读数据时,会被编译器翻译成阻塞模式,block为true
1 | select { |
1.2非阻塞
若在case后加上default则会翻译成非阻塞模式,block为false
1 | select { |
2.源码
1 | // chanrecv在c上接收数据并写入ep, 如果case中的数据接收者为空,则ep为nil(例如 case _, ok <- c:). |
五、注意事项
1.向channel中写数据时len(c)检查缓冲区是否满有隐患
1 | //code1 |
1 | //code2 |
- 情况1:上面代码中,loop单独一个goroutine运行,send只在主线程中调用。
- 情况2:不知什么时候开始,除主线程外,其它goroutine也在调用send。
若情况1遇到handleMsg被阻塞时,return函数会直接返回,不会对主线程造成影响。 如情况2 send函数在多线程调用情况下是不能保证缓冲区满时第6行一定return的。若此时handleMsg被阻塞,那么有概率造成主线程的阻塞。所以在往channel中写数据时,建议默认使用非阻塞方式即:
1 | select { |