深入讨论channel timeout

Go 语言的 channel 本身是不支持 timeout 的,所以一般实现 channel 的读写超时都采用 select,如下:

select {
case <-c:
case <-time.After(time.Second):
}

这两天在写码的过程中突然对这样实现 channel 超时产生了怀疑,这种方式真的好吗?于是我写了这样一个测试程序:

package main

import (
"os"
"time"
) func main() {
c := make(chan int, 100) go func() {
for i := 0; i < 10; i++ {
c <- 1
time.Sleep(time.Second)
} os.Exit(0)
}() for {
select {
case n := <-c:
println(n)
case <-timeAfter(time.Second * 2):
}
}
} func timeAfter(d time.Duration) chan int {
q := make(chan int, 1) time.AfterFunc(d, func() {
q <- 1
println("run") // 重点在这里
}) return q
}

这个程序很简单,你会发现运行结果将会输出 10 次 “run”,也就是每一遍执行 select 注册的 timer 最终都执行了,虽然这里读 channel 都没有超时。原因其实很简单,每次执行 select 语句,都会将 case 条件语句给执行一遍,于是 timeAfter 的执行结果就是会创建一个定时器,并注册到 runtime 中,select 语句执行完成后,这个定时器本身并没有撤销,还继续保留在 runtime 的小顶堆中,所以这些 timer 一超时就会执行挂载的函数。

当然,用 time.After() 函数来做 channel 的读写超时,在应用层根本感受不到底层的定时器还保留着、继续执行;问题是,如果这里的 select 语句在循环中执行得非常快,也就是 channel 中的消息来得非常频繁,会出现的问题就是 runtime 中会有大量的定时器存在,timeout 的时间设置得越长,底层维护的定时器就会越多。原因就是每次 select 都会注册一个新的 timer,并且 timer 只有在它超时后才会被删除。

想想,自己的 channel 每秒钟将传输成千上万的消息,将会有多少 timer 对象存在底层 runtime 中。大量的临时对象会不会影响内存?大量的 timer 会不会影响其他定时器的准确度?

最后,我觉得正确的 channel timeout 也许应该这么做:

to := time.NewTimer(time.Second)
for {
to.Reset(time.Second)
select {
case <-c:
case <-to.C:
}
}

这样做就是为了维护一个全局单一的定时器,每次操作前调整一下定时器的超时时间,从而避免每次循环都生成新的定时器对象。

简单测试了一下两种 channel 超时实现方式,在全力收发数据的情况的内存对象和 gc 情况。 
 
* 蓝线是采用 time.After(),并设置4s 超时的堆内存对象分配的数量 * 绿线是采用 time.After(),并设置2s 超时的堆内存对象分配的数量 * 黄线是采用全局 timer,并设置4s 超时的堆内存对象分配的数量

这个现象其实是预料之中的,重点可以注意设置的超时时间越长,time.After() 的表现将越糟糕。

这三条线和上图的三条线描述的对象是一样的,图中的 gc 时间是平均每次 gc 的时间。

针对这个 channel timeout,我没有去测试是否会影响其他定时器的准确性,但我认为这是必然的,随着定时器的增多。

最后,我始终觉得 channel 本身应该支持超时机制,而不是利用 select 来实现。

另外参见:

如何正确使用 Timer 来完成上面提到的定时任务?

func demo(input chan interface{}) {
t1 := time.NewTimer(time.Second * 5)
t2 := time.NewTimer(time.Second * 10) for {
select {
case msg <- input:
println(msg) case <-t1.C:
println("5s timer")
t1.Reset(time.Second * 5) case <-t2.C:
println("10s timer")
t2.Reset(time.Second * 10)
}
}
}

改正后的程序,原理上是自定义两个全局的 Timer,每次执行 select 都重复使用这两个 Timer,而不是每次都生成全新的。这样才可以真正做到在接收消息的同时,还能够定时的执行相应的任务。


探索任何一个现象背后的真正原因,才是最有趣的事情。

深入讨论channel timeout的更多相关文章

  1. 【转】golang的channel的几种用法

    关闭2次 ch := make(chan bool) close(ch) close(ch)  // 这样会panic的,channel不能close两次 读取的时候channel提前关闭了 ch : ...

  2. Apache Flume 1.7.0 发布,日志服务器

    Apache Flume 1.7.0 发布了,Flume 是一个分布式.可靠和高可用的服务,用于收集.聚合以及移动大量日志数据,使用一个简单灵活的架构,就流数据模型.这是一个可靠.容错的服务. 本次更 ...

  3. Mousejack Hacking : 如何利用MouseJack进行物理攻击

    0×00 前言 近期安全公司Bastille Networks(巴士底狱)安全研究员发现大多数无线鼠标和接收器之间的通信信号是不加密的.黑客可对一百米范围内存在漏洞的蓝牙无线键鼠进行嗅探甚至劫持,从而 ...

  4. The Services(服务)

    datastore和运行时环境的关系就是和一个服务的关系:应用使用API访问一个独立的系统(separate system),这个系统管理应用的所有的独立于应用实例的扩展需求(scaling need ...

  5. yate.conf

    但档案.粘贴下面的例子.不解释!除去非常灵活!只保留sip电话! [general] ; General settings for the operation of Yate ; modload: b ...

  6. 第十三章:Python の 网络编程进阶(二)

    本課主題 SQLAlchemy - Core SQLAlchemy - ORM Paramiko 介紹和操作 上下文操作应用 初探堡垒机 SQLAlchemy - Core 连接 URL 通过 cre ...

  7. Android Wifi 主动扫描 被动扫描

    介绍主动扫描,被动扫描以及连接的wifi的扫描过程 参考文档 <802.11无线网络权威指南> <80_Y0513_1_QCA_WCN36X0_SOFTWARE_ARCHITECTU ...

  8. Golang利用select实现超时机制

    所谓超时,比如上网浏览一些安全的网站,如果几分钟之后不做操作,那么就会让你重新登录.就所谓有时候出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞情况,这时候就可以用select来设置 ...

  9. The Microservices Workflow Automation Cheat Sheet

    Written by Bernd Rücker on Dec 12 2018 in the Best Practices category. Editor’s Note: This post orig ...

随机推荐

  1. 配置使用dwr完成收邮件提示

    DWR(Direct Web Remoting)是一个用于改善web页面与Java类交互的远程服务器端Ajax开源框架,可以帮助开发人员开发包含AJAX技术的网站.它可以允许在浏览器里的代码使用运行在 ...

  2. 1-bit and 2-bit Characters

    We have two special characters. The first character can be represented by one bit 0. The second char ...

  3. CXF 5参考资料

    下载 描述 名字 大小 下载方法 本教程示例应用程序的源代码 CXF_Spring_Survey_Src.war 10 KB HTTP 关于下载方法的信息 学习 Apache CXF 的官方站点:提供 ...

  4. 关于mybatis更新数据的问题

    前两天用mybatis的时候,发现这样一个问题,日志显示mytatis更新数据已经成功了,但是实际上数据库是没有更新到的,经过一番查找,发现mybatis更新的时候默认返回的是查找到的数据(Rows  ...

  5. Day15 jss整体结构梳理

    JS DOM--- 两个步骤: 1 查找标签 (1)直接查找 document.getElementById("idname") // dom对象 document.getElem ...

  6. Java中常用的数据结构类

    结构体系图 List ArrayList.LinkedList.Vector有什么区别? ArrayList 只能装入引用对象(基本类型要转换为封装类): 线程不安全: 底层由数组实现(顺序表),因为 ...

  7. How Microservices are Transforming Python Development

    https://blog.appdynamics.com/engineering/how-microservices-are-transforming-python-development/ Summ ...

  8. Spark学习笔记

    Map-Reduce 我认为上图代表着MapReduce不仅仅包括Map和Reduce两个步骤这么简单,还有两个隐含步骤没有明确,全部步骤包括:切片.转换.聚合.叠加,按照实际的运算场景上述步骤可以简 ...

  9. JS window对象的top、parent、opener含义介绍(转载)

    1.top该变更永远指分割窗口最高层次的浏览器窗口.如果计划从分割窗口的最高层次开始执行命令,就可以用top变量. 2.openeropener用于在window.open的页面引用执行该window ...

  10. 国内各大支付平台的API地址

    1丶目前国内最火的支付平台--蚂蚁金服开放平台(支付宝) https://open.alipay.com/platform/home.htm 2丶国内游戏帝国--腾讯(微信支付) https://pa ...