再次探讨go实现无限 buffer 的 channel方法


Posted in Golang onJune 13, 2021

前言

总所周知,go 里面只有两种 channel,一种是 unbuffered channel, 其声明方式为

ch := make(chan interface{})

另一种是 buffered channel,其声明方式为

bufferSize := 5
ch := make(chan interface{},bufferSize)

对于一个 buffered channel,无论它的 buffer 有多大,它终究是有极限的。这个极限就是该 channel 最初被 make 时,所指定的 bufferSize 。

jojo,buffer channel 的大小是有极限的,我不做 channel 了。

一旦 channel 满了的话,再往里面添加元素的话,将会阻塞。

so how can we make a infinite buffer channel?

本文参考了 medinum 上面的一篇文章,有兴趣的同学可以直接阅读原文。

实现

接口的设计

首先当然是建一个 struct,在百度翻译的帮助下,我们将这个 struct 取名为 InfiniteChannel

type InfiniteChannel struct {
}

思考一下 channel 的核心行为,实际上就两个,一个流入(Fan in),一个流出(Fan out),因此我们添加如下几个 method。

func (c *InfiniteChannel) In(val interface{}) {
	// todo
}

func (c *InfiniteChannel) Out() interface{} {
	// todo
}

内部实现

通过 In() 接收的数据,总得需要一个地方来存放。我们可以用一个 slice 来存放,就算用 In() 往里面添加了很多元素,也可以通过 append() 来拓展 sliceslice 的容量可以无限拓展下去(内存足够的话),所以 channel 也是 infiniteInfiniteChannel 的第一个成员就这么敲定下来的。

type InfiniteChannel struct {
	data    []interface{}
}

用户调用 In()Out() 时,可能是并发的环境,在 go 中如何进行并发编程,最容易想到的肯定是 channel 了,因此我们在内部准备两个 channel,一个 inChan,一个 outChan,用 inChan 来接收数据,用 outChan 来流出数据。

type InfiniteChannel struct {
	inChan  chan interface{}
	outChan chan interface{}
	data    []interface{}
}

func (c *InfiniteChannel) In(val interface{}) {
	c.inChan <- val
}

func (c *InfiniteChannel) Out() interface{} {
	return <-c.outChan
}

其中, inChanoutChan 都是 unbuffered channel。

此外,也肯定是需要一个 select 来处理来自 inChanoutChan 身上的事件。因此我们另起一个协程,在里面做 select 操作。

func (c *InfiniteChannel) background() {
	for true {
		select {
		case newVal := <-c.inChan:
			c.data = append(c.data, newVal)
        case c.outChan <- c.pop():		// pop() 将取出队列的首个元素
		}
	}
}
func NewInfiniteChannel() *InfiniteChannel {
	c := &InfiniteChannel{
		inChan:  make(chan interface{}),
		outChan: make(chan interface{}),
	}
	go c.background()	// 注意这里另起了一个协程
	return c
}

ps:感觉这也算是 go 并发编程的一个套路了。即

  1. 在 new struct 的时候,顺手 go 一个 select 协程,select 协程内执行一个 for 循环,不停的 select,监听一个或者多个 channel 的事件。
  2. struct 对外提供的 method,只会操作 struct 内的 channel(在本例中就是 inChan 和 outChan),不会操作 struct 内的其他数据(在本例中,In() 和 Out() 都没有直接操作 data)。
  3. 触发 channel 的事件后,由 select 协程进行数据的更新(在本例中就是 data )。因为只有 select 协程对除 channel 外的数据成员进行读写操作,且 go 保证了对于 channel 的并发读写是安全的,所以代码是并发安全的。
  4. 如果 struct 是 exported ,用户或许会越过 new ,直接手动 make 一个 struct,可以考虑将 struct 设置为 unexported,把它的首字母小写即可。

pop() 的实现也非常简单。

// 取出队列的首个元素,如果队列为空,将会返回一个 nil
func (c *InfiniteChannel) pop() interface{} {
	if len(c.data) == 0 {
		return nil
	}
	val := c.data[0]
	c.data = c.data[1:]
	return val
}

测试一下

用一个协程每秒钟生产一条数据,另一个协程每半秒消费一条数据,并打印。

func main() {
	c := NewInfiniteChannel()
	go func() {
		for i := 0; i < 20; i++ {
			c.In(i)
			time.Sleep(time.Second)
		}
	}()

	for i := 0; i < 50; i++ {
		val := c.Out()
		fmt.Print(val)
		time.Sleep(time.Millisecond * 500)
	}
}
// out
<nil>0<nil>1<nil>23<nil>4<nil><nil>5<nil>67<nil><nil>89<nil><nil>1011<nil>12<nil>13<nil>14<nil>15<nil>16<nil>17<nil><nil>1819<nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil>
Process finished with the exit code 0

可以看到,将 InfiniteChannel 内没有数据可供消费时,调用 Out() 将会返回一个 nil,不过这也在我们的意料之中,原因是 pop() 在队列为空时,将会返回 nil。

目前 InfiniteChannel 的行为与标准的 channel 的行为是有出入的,go 中的 channel,在没有数据却仍要取数据时会被阻塞,如何实现这个效果?

优化

我认为此处是是整篇文章最有技巧的地方,我第一次看到时忍不住拍案叫绝。

首先把原来的 background() 摘出来

func (c *InfiniteChannel) background() {
	for true {
		select {
		case newVal := <-c.inChan:
			c.data = append(c.data, newVal)
		case c.outChan <- c.pop():
		}
	}
}

outChan 进行一个简单封装

func (c *InfiniteChannel) background() {
	for true {
		select {
		case newVal := <-c.inChan:
			c.data = append(c.data, newVal)
		case c.outChanWrapper() <- c.pop():
		}
	}
}
func (c *InfiniteChannel) outChanWrapper() chan interface{} {
	return c.outChan
}

目前为止,一切照旧。

点睛之笔来了:

func (c *InfiniteChannel) outChanWrapper() chan interface{} {
	if len(c.data) == 0 {
		return nil
	}
	return c.outChan
}

c.data 为空的时候,返回一个 nil

background() 中,当执行到 case c.outChan <- c.pop(): 时,实际上将会变成:

case nil <- nil:

go 中,是无法往一个 nilchannel 中发送元素的。例如

func main() {
	var c chan interface{}
	select {
	case c <- 1:
	}
}
// fatal error: all goroutines are asleep - deadlock!
func main() {
	var c chan interface{}
	select {
	case c <- 1:
	default:
		fmt.Println("hello world")
	}
}
// hello world

因此,对于

select {
case newVal := <-c.inChan:
	c.data = append(c.data, newVal)
case c.outChanWrapper() <- c.pop():
}

将会一直阻塞在 select 那里,直到 inChan 来了数据。

再测试一下

012345678910111213141516171819fatal error: all goroutines are asleep - deadlock!

最后,程序 panic 了,因为死锁了。

补充

实际上 channel 除了 In()Out() 外,还有一个行为,即 close(),如果 channel close 后,依旧从其中取元素的话,将会取出该类型的默认值。

func main() {
	c := make(chan interface{})
	close(c)
	for true {
		v := <-c
		fmt.Println(v)
		time.Sleep(time.Second)
	}
}
// output
// <nil>
// <nil>
// <nil>
// <nil>
func main() {
	c := make(chan interface{})
	close(c)
	for true {
		v, isOpen := <-c
		fmt.Println(v, isOpen)
		time.Sleep(time.Second)
	}
}
// output
// <nil> false
// <nil> false
// <nil> false
// <nil> false

我们也需要实现相同的效果。

func (c *InfiniteChannel) Close() {
	close(c.inChan)
}

func (c *InfiniteChannel) background() {
	for true {
		select {
		case newVal, isOpen := <-c.inChan:
			if isOpen {
				c.data = append(c.data, newVal)
			} else {
				c.isOpen = false
			}
		case c.outChanWrapper() <- c.pop():
		}
	}
}

func NewInfiniteChannel() *InfiniteChannel {
	c := &InfiniteChannel{
		inChan:  make(chan interface{}),
		outChan: make(chan interface{}),
		isOpen:  true,
	}
	go c.background()
	return c
}

func (c *InfiniteChannel) outChanWrapper() chan interface{} {
    // 这里添加了对 c.isOpen 的判断
	if c.isOpen && len(c.data) == 0 {
		return nil
	}
	return c.outChan
}

再测试一下

func main() {
	c := NewInfiniteChannel()
	go func() {
		for i := 0; i < 20; i++ {
			c.In(i)
			time.Sleep(time.Second)
		}
		c.Close()		// 这里调用了 Close
	}()

	for i := 0; i < 50; i++ {
		val := c.Out()
		fmt.Print(val)
		time.Sleep(time.Millisecond * 500)
	}
}
// output
012345678910111213141516171819<nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil><nil>
Process finished with the exit code 0

符合预期

遗憾

目前看上去已经很完美了,但是和标准的 channel 相比,仍然有差距。因为标准的 channel 是有这种用法的

v,isOpen := <- ch

可以通过 isOpen 变量来获取 channel 的开闭情况。

因此 InfiniteChannel 也应该提供一个类似的 method

func (c *InfiniteChannel) OutAndIsOpen() (interface{}, bool) {
	// todo
}

可惜的是,要想得知 InfiniteChannel 是否是 Open 的,就必定要访问 InfiniteChannel 内的 isOpen 成员。

type InfiniteChannel struct {
	inChan  chan interface{}
	outChan chan interface{}
	data    []interface{}
	isOpen  bool
}

isOpen 并非 channel 类型,根据之前的套路,这种非 channel 类型的成员只应该被 select 协程访问。一旦有多个协程访问,就会出现并发问题,除非加锁。

我不能接受!所以干脆不提供这个 method 了,嘿嘿。

完整代码

func main() {
	c := NewInfiniteChannel()
	go func() {
		for i := 0; i < 20; i++ {
			c.In(i)
			time.Sleep(time.Second)
		}
		c.Close()
	}()

	for i := 0; i < 50; i++ {
		val := c.Out()
		fmt.Print(val)
		time.Sleep(time.Millisecond * 500)
	}
}

type InfiniteChannel struct {
	inChan  chan interface{}
	outChan chan interface{}
	data    []interface{}
	isOpen  bool
}

func (c *InfiniteChannel) In(val interface{}) {
	c.inChan <- val
}

func (c *InfiniteChannel) Out() interface{} {
	return <-c.outChan
}

func (c *InfiniteChannel) Close() {
	close(c.inChan)
}

func (c *InfiniteChannel) background() {
	for true {
		select {
		case newVal, isOpen := <-c.inChan:
			if isOpen {
				c.data = append(c.data, newVal)
			} else {
				c.isOpen = false
			}
		case c.outChanWrapper() <- c.pop():
		}
	}
}

func NewInfiniteChannel() *InfiniteChannel {
	c := &InfiniteChannel{
		inChan:  make(chan interface{}),
		outChan: make(chan interface{}),
		isOpen:  true,
	}
	go c.background()
	return c
}

// 取出队列的首个元素,如果队列为空,将会返回一个 nil
func (c *InfiniteChannel) pop() interface{} {
	if len(c.data) == 0 {
		return nil
	}
	val := c.data[0]
	c.data = c.data[1:]
	return val
}

func (c *InfiniteChannel) outChanWrapper() chan interface{} {
	if c.isOpen && len(c.data) == 0 {
		return nil
	}
	return c.outChan
}

参考

https://medium.com/capital-one-tech/building-an-unbounded-channel-in-go-789e175cd2cd

以上就是再次探讨go实现无限 buffer 的 channel方法的详细内容,更多关于go无限 buffer 的 channel的资料请关注三水点靠木其它相关文章!

Golang 相关文章推荐
Go各时间字符串使用解析
Apr 02 Golang
Go语言操作数据库及其常规操作的示例代码
Apr 21 Golang
使用Golang的channel交叉打印两个数组的操作
Apr 29 Golang
golang日志包logger的用法详解
May 05 Golang
Golang中异常处理机制详解
Jun 08 Golang
简单聊聊Golang中defer预计算参数
Mar 25 Golang
Golang数据类型和相互转换
Apr 12 Golang
golang生成并解析JSON
Apr 14 Golang
golang定时器
Apr 14 Golang
Golang 字符串的常见操作
Apr 19 Golang
Go获取两个时区的时间差
Apr 20 Golang
Go gorilla/sessions库安装使用
Aug 14 Golang
Go遍历struct,map,slice的实现
Jun 13 #Golang
go web 预防跨站脚本的实现方式
Jun 11 #Golang
Golang生成Excel文档的方法步骤
Go timer如何调度
浅谈Golang 切片(slice)扩容机制的原理
Jun 09 #Golang
Golang中异常处理机制详解
Go语言实现Snowflake雪花算法
Jun 08 #Golang
You might like
php解决约瑟夫环示例
2014/04/09 PHP
PHP判断手机是IOS还是Android
2015/12/09 PHP
Lumen timezone 时区设置方法(慢了8个小时)
2018/01/20 PHP
不懂JavaScript应该怎样学
2008/04/16 Javascript
获取当前网页document.url location.href区别总结
2008/05/10 Javascript
用tip解决Ext列宽度不够的问题
2008/12/13 Javascript
javascript 节点排序 2
2011/01/31 Javascript
JavaScript学习笔记记录我的旅程
2012/05/23 Javascript
jquery ajax方式直接提交整个表单核心代码
2013/08/15 Javascript
深入解析Javascript闭包的功能及实现方法
2016/07/10 Javascript
Js删除数组中某一项或几项的几种方法(推荐)
2016/07/27 Javascript
js实现精确到毫秒的倒计时效果
2016/08/05 Javascript
AngularJS bootstrap启动详解及实例代码
2016/09/14 Javascript
jQuery判断是否存在滚动条的简单方法
2016/09/17 Javascript
一个可复用的vue分页组件
2017/05/15 Javascript
jquery+css3实现熊猫tv导航代码分享
2018/02/12 jQuery
VUE-cli3使用 svg-sprite-loader
2018/10/20 Javascript
详解javascript设计模式三:代理模式
2019/03/25 Javascript
js JSON.stringify()基础详解
2019/06/19 Javascript
解决Vue-cli3没有vue.config.js文件夹及配置vue项目域名的问题
2020/12/04 Vue.js
使用python实现rsa算法代码
2016/02/17 Python
详解Python 实现元胞自动机中的生命游戏(Game of life)
2018/01/27 Python
如何通过Python实现标签云算法
2019/07/02 Python
解决python ThreadPoolExecutor 线程池中的异常捕获问题
2020/04/08 Python
如何解决pycharm调试报错的问题
2020/08/06 Python
如何使用pycharm连接Databricks的步骤详解
2020/09/23 Python
html5简单示例_动力节点Java学院整理
2017/07/07 HTML / CSS
法国时尚品牌乐都特瑞士站:La Redoute瑞士
2016/09/05 全球购物
领导干部考察材料
2014/02/08 职场文书
电子商务专业求职信
2014/03/08 职场文书
2014副局长群众路线对照检查材料思想汇报
2014/09/22 职场文书
升职自我推荐信范文
2015/03/25 职场文书
《叶问2》观后感
2015/06/15 职场文书
公司岗位说明书
2015/10/08 职场文书
2019终止劳动合同协议书最新范本!
2019/07/09 职场文书
http通过StreamingHttpResponse完成连续的数据传输长链接方式
2022/02/12 Python