Go系列 --channel 及其介绍

 

序言

go 中以 goroutine 为最小并发单元,具有比线程更小的开销,使得go的并发和切换性能较强。 而goroutine 之间的相互通信就需要靠 channel来实现。不像 一般语言,线程之间共享信息的方式是通过共享内存方式来进行,

为了保护数据安全,通常访问时候需要加锁,或者采用乐观锁技术(CAS比较),当然go也实现了共享内存加锁方式。 而channel 就像是信道,两个不同的goroutine 相互通信时候,一方充当消费者,另一方充当生产者,信道就像是消息队列

有FIFO的性质,go 能保证channel 的消息不会有丢失的可能。 下面就以channel的使用 和其 内部实现来讲起。

 

 

正文

 channel 传递的意义

结论:

值得注意的是,channel 设计的本意是在两个goroutine 进行消息传递,如果有多个 goroutine 同时尝试 消费同一个channel 的信息,那么这些goroutine 之间表现一定是竞争关系。

同一个channel在不同的routine 之间共享的话,channel的消息传递消费 是单发模型,点对点的,有多个同时消费的routine则表现为竞争,只有一个routine能被通知到。

 

联想拓展:

在共享内存并发模型中, 比如 java中,我们对一个 共享变量进行 sychronized 加锁

 

共享变量: commonLock
Thread A:
sychronized(commonLock){
// action A
}

Thread B:
sychronized(commonLock){
// action B
}

Thread C:
sychronized(commonLock){
// action C
}


 

这里的话,假如A先执行,那么其他 B,C线程进入等待, 当 A执行完, jvm 的调度原则,会自动唤醒 B,C中的其中一个线程继续执行, 如果其中又执行完,那么会继续通知另一个等待线程继续执行。

这里可以看到,在等待一个共享变量锁的同时,这个锁上应该依附了等待的 线程队列信息,每次 离开锁的区域, jvm 会负责调度唤醒另一个 线程进入同步块执行。 这相当于 是 jvm 对于一个共享锁,帮我们进行了队列的逐步唤醒调度。

在这里就不提 object.wait() 释放锁资源,object.notify()通知竞争锁资源等行为了, Thread 之间的 状态,是否休眠,是否可运行,是否等待中,这里也是一个有限状态机,这里先不提。

 

而回到go 语言的channel ,同样是通知,它的存在意义在于两个gorutine之间的通信, 如果有多个routine ,那么消费者会表现为竞争关系,这其实就不是共享锁 调度队列模型了,那么我们想想go 语言到底帮我们维护了什么呢,我们到底为啥要用这个channel,也就是所谓的CSP并发模型到底简单在哪,在这里给出回答, channel 顾名思义 它是一个管道,是一个双向通道,当有不同的goroutine 投递消息和消费消息的时候,它能保证消息不丢,能被消费到,这个模型其实就是 消息队列中的 生产者 和 消费组的概念

 

p1     ---m1-->                                      ---->   c1

p2     ---m2-->        channel               ----->c2

p3     ----m3-->                                  ------>  c3

 

p1,p2,p3 三个 goroutine 投递的消息, go保证了消息能投递进channel ,go 保证了 每次 c1,c2,c3 三个goroutine 中的其中一个能消费到其中的一个投递的消息,然后多个消息重复这个流程

( 拓展: 在消息队列概念中, (c1,c2,c3)同属于一个消费组,一个消费组内,每次只有一个消费者能消费,属于单播模式, 同样的消息队列概念里面,如果有多个消费组,那么消费组之间 和生产者关系即为广播关系)

 

 

群发模型, 或者是观察者模型,并不是通过一个channel解决的问题,可以通过 channel 数组 来进行。不同goroutine 消费不同的channel,发送信息时候,生产者往 [] channel 里面塞数据。

值得注意的是: 当 调用 close(channel)的时候,所有goroutine 都会得到通知 ,va,ok:= ←   channel   返回的值 ok 就为 false. 这时候是广播的效用, context 组件就利用了这个 context.Done() 实现了 树形 goroutine 的层级停用。 从 channel 的底层结构里面,channle 也有正在等待 channel信号的goroutine 列表,所以实现广播也是可行的。

 

  1. 类似于生产消费者模型,我们很容易产生类比,确实从使用上有确有其类似之处。首先channel 分为 有缓冲区和无缓冲区 两种,表现也不一致。其实也可以统一起来就是,无缓冲区就是缓冲区为0的 channel,其生产者需要依靠消费者的消费,

没有消费者消费的时候,生产者的消息也发不出去。

 

channel的使用

 

func main() {
	withoutBuffer()
	withBuffer()
	//selectAndDefaultUse()
	//rangeUseChannelConsume()
	time.Sleep(time.Minute)
}

func withBuffer() {
	defer fmt.Print(enterAndLeaveCall("withBuffer "))
	produceAndConsumer(3)

}
func withoutBuffer() {
	defer fmt.Print(enterAndLeaveCall("withoutBuffer "))
	produceAndConsumer(0)
}

func produceAndConsumer(buffer int) {
	tmpChan := make(chan int, buffer)

	go func() {
		for i := 0; i < 3; i++ {
			tmpChan <- i
			fmt.Printf("produce %v\n", i)
		}
	}()

	consuFun := func(tag string) {
		fmt.Printf("tag %v, and consume %v\n", tag, <-tmpChan)
	}

	go consuFun("c1")
	go consuFun("c2")
	time.Sleep(4 * time.Second)
}

 

 

其输出为

 

enter  withoutBuffer <<<<<
produce 0
tag c2, and consume 0
tag c1, and consume 1
produce 1

leave  withoutBuffer >>>>>>

enter  withBuffer <<<<<
produce 0
produce 1
produce 2
tag c1, and consume 0
tag c2, and consume 1

leave  withBuffer >>>>>>

可以明显看到,如果没有带缓冲区, 生产者会等待有消费者消费的时候,才能继续生产,不然会直接阻塞。

 

channel的取值还可以通过 select 和 range 等关键字组合

 

如果是使用 select 可以使用超时机制,防止等待 channel 数据过久

 

 

func selectAndDefaultUse() {
	tmpChan := make(chan int, 3)
	go func() {
		for i := 0; i < 10; i++ {
			tmpChan <- i
			time.Sleep(time.Second)
		}
	}()

	go func() {
		for {
			select {
			case a1 := <-tmpChan:
				fmt.Printf("select case value is %v \n", a1)

			case <-time.After(300 * time.Millisecond):
				fmt.Println("after duration ")

				//default:
				//	fmt.Printf("default case\n ")
				//	time.Sleep(300 * time.Millisecond)

			}
		}
	}()

}

 

如上, select case 中 加上了一个 time.After 意思是在 这个时间段内如果没有接受到 来自channel的数据,就会走这个分支。而default 也是很明显的意义,就是 如果当下 其他分支channel都还没有数据到达,会直接走default 分支。

 

range 的使用,直接把channel 里面的数据,通过循环语法糖的方式打印出来

 

 

func rangeUseChannelConsume() {
	defer fmt.Println(enterAndLeaveCall("rangeUseChannelConsume"))

	waitDone := sync.WaitGroup{}
	tmpChan := make(chan int, 3)
	waitDone.Add(10)
	go func() {
		for i := 0; i < 10; i++ {
			tmpChan <- i
			time.Sleep(time.Second)
		}
	}()

	go func() {
		for a1 := range tmpChan {
			fmt.Println("range use ", a1)
			waitDone.Done()
		}
	}()
	waitDone.Wait()
}

 

 

输出为

比较容易理解,在此不表。

enter  rangeUseChannelConsume<<<<<
range use  0
range use  1
range use  2
range use  3
range use  4
range use  5
range use  6
range use  7
range use  8
range use  9

leave  rangeUseChannelConsume>>>>>>

 

 

 

channel 的内部实现

channel 内存结构表现

 

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
}

type waitq struct {
	first *sudog
	last  *sudog
}

type sudog struct {
	// The following fields are protected by the hchan.lock of the
	// channel this sudog is blocking on. shrinkstack depends on
	// this for sudogs involved in channel ops.

	g *g

	// isSelect indicates g is participating in a select, so
	// g.selectDone must be CAS'd to win the wake-up race.
	isSelect bool
	next     *sudog
	prev     *sudog
	elem     unsafe.Pointer // data element (may point to stack)

	// The following fields are never accessed concurrently.
	// For channels, waitlink is only accessed by g.
	// For semaphores, all fields (including the ones above)
	// are only accessed when holding a semaRoot lock.

	acquiretime int64
	releasetime int64
	ticket      uint32
	parent      *sudog // semaRoot binary tree
	waitlink    *sudog // g.waiting list or semaRoot
	waittail    *sudog // semaRoot
	c           *hchan // channel
}

 

其中  比较重要的是, 有等待发送的 goroutine 的双向链表 sendq, 等待接收值的goroutine 的双向链表 recvq.  

  • qcount — Channel 中的元素个数;
  • dataqsiz — Channel 中的循环队列的长度;
  • buf — Channel 的缓冲区数据指针;
  • sendx — Channel 的发送操作处理到的位置;
  • recvx — Channel 的接收操作处理到的位置;
  • elemsize 收发元素大小
  • elemtype 收发元素类型
  • sendq 当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,发送goroutine阻塞
  • recvq 当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,接收goroutine阻塞

 

这些等待队列使用双向链表 runtime.waitq 表示,链表中所有的元素都是 runtime.sudog 结构

 

从channel 调用close(channel)操作函数来看, close channel 会给 收发 goroutine 列表都发送关闭消息

 

 

func closechan(c *hchan) {
	if c == nil {
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}

	if raceenabled {
		callerpc := getcallerpc()
		racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
		racerelease(c.raceaddr())
	}

	c.closed = 1

	var glist gList

	// release all readers
	for {
		sg := c.recvq.dequeue()
		if sg == nil {
			break
		}
		if sg.elem != nil {
			typedmemclr(c.elemtype, sg.elem)
			sg.elem = nil
		}
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		gp := sg.g
		gp.param = nil
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp)
	}

	// release all writers (they will panic)
	for {
		sg := c.sendq.dequeue()
		if sg == nil {
			break
		}
		sg.elem = nil
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		gp := sg.g
		gp.param = nil
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp)
	}
	unlock(&c.lock)

	// Ready all Gs now that we've dropped the channel lock.
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

 

简而言之,就是 收消息的goroutine 列表能收到关闭消息, 发送消息的goroutine 会 panic

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值