channel是什么?
golang语言中,channel是一个协程安全的FIFO的队列,读取和写入操作都是原子操作
使用场景
用来做多协程之间的通信,java中的线程之间的通信是通过共享内存实现的,A线程获取内存区域,并且“锁”住内存这块区域,然后执行临界区代码,这一时刻无法获取锁的其他线程阻塞,直到A线程释放锁(实际上就是释放刚才锁住的内存区域),其他线程继续竞争共享内存,获取锁的执行临界区代码。这个过程本质上是通过共享内存的方式实现多线程通信。而golang提出了新的通信方式:用通信来共享内存,而不要用共享内存来通信
使用方式
##无缓冲区的channel
创建 var NoRoutChannel chan 【类型】= make(chan 【类型】)
使用场景
只是作为信号的channel,告诉另一个协程,这件事我做完了,而不需要给另外的协程序发送做完后的通知内容
##有缓冲区的channel
创建 var NoRoutChannel chan 【类型】= make(chan 【类型】,【缓冲大小】)
使用场景
告诉另一个协程,这件事我做完了,并且发送和另一个协程序传递的变量
PS:注意channel读取和写入操作必须在两个不同的协程中进行,否则panic
channel状态
channel分为nil、open、closed
对于nil的channel无论读写都panic,对于closed状态的channel,向里边push的操作会报panic
demo: 使用channel实现一个生产者消费者模式
var wg sync.WaitGroup = sync.WaitGroup{}
type Product struct {
Queue chan string
}
func (product *Product) product(apple string){
fmt.Println("product: "+apple)
product.Queue <- apple
}
type Consumer struct {
Queue chan string
}
func (consumer *Consumer) Consume(){
con := <- consumer.Queue
fmt.Println("consume:"+con)
defer wg.Done()
}
func main() {
testChan := make(chan string,1)
p := Product{
Queue: testChan,
}
c := Consumer{
Queue: testChan,
}
go c.Consume()
go p.product("3333")
wg.Add(1)
wg.Wait()
fmt.Println("end job")
}
channel内部原理
数据结构
type hchan struct {
qcount uint // 所有在队列中数据数量
dataqsiz uint // 环形队列大小,可以存放的元素个数
buf unsafe.Pointer // 只想环形队列的指针
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // 生产下标
recvx uint // 消费下标
recvq waitq // 消费者队列
sendq waitq // 生产者队列
// 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缓冲区实现--环形队列
chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个元素的channel示意图:
- dataqsiz指示了队列长度为6,即可缓存6个元素;
- buf指向队列的内存,队列中还剩余两个元素;
- qcount表示队列中还有两个元素;
- sendx指示后续写入的数据存储的位置,取值[0, 6);
- recvx指示从该位置读取数据, 取值[0, 6);
等待队列
从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中:
- 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
- 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;
下图展示了一个没有缓冲区的channel,有几个goroutine阻塞等待读数据:
注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据。
make(chan string,2)分析
代码:
func makechan(t *chantype, size int) *hchan {
var c hchan
c = new(hchan)
c.buf = malloc(元素类型大小size)
c.elemsize = 元素类型大小
c.elemtype = 元素类型
c.dataqsiz = size
return c
}
向channel写数据
向一个channel中写数据简单过程如下:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;
简单流程图如下:
从channel中读取数据
从一个channel读数据简单过程如下:
- 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
- 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;
简单流程图如下:
关闭channel
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。
除此之外,panic出现的常见场景还有:
关闭值为nil的channel
关闭已经被关闭的channel
向已经关闭的channel写数据