golang channel详解

哈哈_dfde · · 767 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

image.png

对于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示意图:

image
  • 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阻塞等待读数据:

image

注意,一般情况下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中写数据简单过程如下:

  1. 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
  2. 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
  3. 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;

简单流程图如下:


image

从channel中读取数据

从一个channel读数据简单过程如下:

  1. 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
  2. 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  4. 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;

简单流程图如下:


image

关闭channel

关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。

除此之外,panic出现的常见场景还有:

关闭值为nil的channel
关闭已经被关闭的channel
向已经关闭的channel写数据


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:哈哈_dfde

查看原文:golang channel详解

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

767 次点击  ∙  1 赞  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传