最近用Timer踩了一个坑,分享一下避免别人继续踩

简介:

  最近做一个小项目,项目中有一个定时服务,需要向对方定时发送数据,时间间隔是1.5s,然后就想到了用C#的Timer类,我们知道Timer

确实非常好用,因为里面有非常人性化的start和stop功能,在Timer里面还有一个Interval,就是用来设置时间间隔,然后时间间隔到了就会触

发Elapsed事件,我们只需要把callback函数注册到这个事件就可以了,如果Interval到了就会触发Elapsed,貌似一切看起来很顺其自然,但是

有一点一定要注意,callback函数本身执行也是需要时间的,也许这个时间是1s,2s或者更长时间,而timer类却不管这些,它只顾1.5s触发一下

Elapsed,这就导致了我的callback可能还没有执行完,下一个callback又开始执行了,也就导致了没有达到我预期的1.5s的效果,并且还出现了

一个非常严重的问题,那就是线程激增,非常恐怖。

 

   下面举个例子,为了简化一下,我就定义一个task任务,当然项目中是多个task任务一起跑的。

 

一:问题产生

   为了具有更高的灵活性,我定义了一个CustomTimer类继承自Timer,然后里面可以放些Task要跑的数据,这里就定义一个Queue。

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            TimerCustom timer = new TimerCustom();

            timer.Interval = 1500;

            timer.Elapsed += (obj, evt) =>
            {
                TimerCustom singleTimer = obj as TimerCustom;

                if (singleTimer != null)
                {
                    if (singleTimer.queue.Count != 0)
                    {
                        var item = singleTimer.queue.Dequeue();

                        Send(item);
                    }
                }
            };

            timer.Start();

            Console.Read();
        }

        static void Send(int obj)
        {
            //随机暂定8-10s
            Thread.Sleep(new Random().Next(8000, 10000));

            Console.WriteLine("当前时间:{0},定时数据发送成功!", DateTime.Now);
        }
    }

    class TimerCustom : System.Timers.Timer
    {
        public Queue<int> queue = new Queue<int>();

        public TimerCustom()
        {
            for (int i = 0; i < short.MaxValue; i++)
            {
                queue.Enqueue(i);
            }
        }
    }
}

 

二:解决方法

1.  从上图看,在一个任务的情况下就已经有14个线程了,并且在21s的时候有两个线程同时执行了,我的第一反应就是想怎么把后续执行callback的

线程踢出去,也就是保证当前仅让两个线程在用callback,一个在执行,一个在等待执行,如果第一个线程的callback没有执行完,后续如果来了第三

个线程的话,我就把这第三个线程直接踢出去,直到第一个callback执行完后,才允许第三个线程进来并等待执行callback,然后曾今的第二个线程开

始执行callback,后续的就以此类推。。。

然后我就想到了用lock机制,在customTimer中增加lockMe,lockNum,isFirst字段,用lockMe来锁住,用lockNum来踢当前多余的要执行callback

的线程,用isFirst来判断是不是第一次执行该callback,后续callback的线程必须先等待1.5s再执行。

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            TimerCustom timer = new TimerCustom();

            timer.Interval = 1500;

            timer.Elapsed += (obj, evt) =>
            {
                TimerCustom singleTimer = obj as TimerCustom;

                if (singleTimer != null)
                {
                    //如果当前等待线程>2,就踢掉该线程
                    if (Interlocked.Read(ref singleTimer.lockNum) > 2)
                        return;

                    Interlocked.Increment(ref singleTimer.lockNum);

                    //这里的lock只能存在一个线程等待
                    lock (singleTimer.lockMe)
                    {
                        if (!singleTimer.isFirst)
                        {
                            Thread.Sleep((int)singleTimer.Interval);
                        }

                        singleTimer.isFirst = false;

                        if (singleTimer.queue.Count != 0)
                        {
                            var item = singleTimer.queue.Dequeue();

                            Send(item);

                            Interlocked.Decrement(ref singleTimer.lockNum);
                        }
                    }
                }
            };

            timer.Start();

            Console.Read();
        }

        static void Send(int obj)
        {
            Thread.Sleep(new Random().Next(8000, 10000));

            Console.WriteLine("当前时间:{0},邮件发送成功!", DateTime.Now);
        }
    }

    class TimerCustom : System.Timers.Timer
    {
        public Queue<int> queue = new Queue<int>();

        public object lockMe = new object();

        public bool isFirst = true;

        /// <summary>
        /// 为保持连贯性,默认锁住两个
        /// </summary>
        public long lockNum = 0;

        public TimerCustom()
        {
            for (int i = 0; i < short.MaxValue; i++)
            {
                queue.Enqueue(i);
            }
        }
    }
}

 

从图中可以看到,已经没有同一秒出现重复任务的发送情况了,并且线程也给压制下去了,乍一看效果不是很明显,不过这是在一个任务的情况

下的场景,任务越多就越明显了,所以这个就达到我要的效果。

 

2. 从上面的解决方案来看,其实我们的思维已经被问题约束住了,当时我也是这样,毕竟坑出来了,就必须来填坑,既然在callback中出现线程

  蜂拥的情况,我当然要想办法管制了,其实这也没什么错,等问题解决了再回头考虑下时,我们会发现文章开头说的Timer类有强大的Stop和

   Start功能,所以。。。。这个时候思维就跳出来了,何不在callback执行的时候把Timer关掉,执行完callback后再把Timer开启,这样不就

   可以解决问题吗?好吧,说干就干。

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            TimerCustom timer = new TimerCustom();

            timer.Interval = 1500;

            timer.Elapsed += (obj, evt) =>
            {
                TimerCustom singleTimer = obj as TimerCustom;

                //先停掉
                singleTimer.Stop();

                if (singleTimer != null)
                {
                    if (singleTimer.queue.Count != 0)
                    {
                        var item = singleTimer.queue.Dequeue();

                        Send(item);

                        //发送完成之后再开启
                        singleTimer.Start();
                    }
                }
            };

            timer.Start();

            Console.Read();
        }

        static void Send(int obj)
        {
            Thread.Sleep(new Random().Next(8000, 10000));

            Console.WriteLine("当前时间:{0},邮件发送成功!", DateTime.Now);
        }
    }

    class TimerCustom : System.Timers.Timer
    {
        public Queue<int> queue = new Queue<int>();

        public object lockMe = new object();

        /// <summary>
        /// 为保持连贯性,默认锁住两个
        /// </summary>
        public long lockNum = 0;

        public TimerCustom()
        {
            for (int i = 0; i < short.MaxValue; i++)
            {
                queue.Enqueue(i);
            }
        }
    }
}

 

从图中可以看到,问题同样得到解决,而且更简单,精妙。


最后总结一下:解决问题的思维很重要,但是如果跳出思维站到更高的抽象层次上考虑问题貌似也很难得。。。

相关文章
|
5天前
|
存储 JavaScript 前端开发
JavaScript基础
本节讲解JavaScript基础核心知识:涵盖值类型与引用类型区别、typeof检测类型及局限性、===与==差异及应用场景、内置函数与对象、原型链五规则、属性查找机制、instanceof原理,以及this指向和箭头函数中this的绑定时机。重点突出类型判断、原型继承与this机制,助力深入理解JS面向对象机制。(238字)
|
4天前
|
云安全 人工智能 安全
阿里云2026云上安全健康体检正式开启
新年启程,来为云上环境做一次“深度体检”
1600 6
|
6天前
|
安全 数据可视化 网络安全
安全无小事|阿里云先知众测,为企业筑牢防线
专为企业打造的漏洞信息收集平台
1329 2
|
5天前
|
缓存 算法 关系型数据库
深入浅出分布式 ID 生成方案:从原理到业界主流实现
本文深入探讨分布式ID的生成原理与主流解决方案,解析百度UidGenerator、滴滴TinyID及美团Leaf的核心设计,涵盖Snowflake算法、号段模式与双Buffer优化,助你掌握高并发下全局唯一ID的实现精髓。
350 160
|
5天前
|
人工智能 自然语言处理 API
n8n:流程自动化、智能化利器
流程自动化助你在重复的业务流程中节省时间,可通过自然语言直接创建工作流啦。
409 6
n8n:流程自动化、智能化利器
|
7天前
|
人工智能 API 开发工具
Skills比MCP更重要?更省钱的多!Python大佬这观点老金测了一周终于懂了
加我进AI学习群,公众号右下角“联系方式”。文末有老金开源知识库·全免费。本文详解Claude Skills为何比MCP更轻量高效:极简配置、按需加载、省90% token,适合多数场景。MCP仍适用于复杂集成,但日常任务首选Skills。推荐先用SKILL.md解决,再考虑协议。附实测对比与配置建议,助你提升效率,节省精力。关注老金,一起玩转AI工具。
|
14天前
|
机器学习/深度学习 安全 API
MAI-UI 开源:通用 GUI 智能体基座登顶 SOTA!
MAI-UI是通义实验室推出的全尺寸GUI智能体基座模型,原生集成用户交互、MCP工具调用与端云协同能力。支持跨App操作、模糊语义理解与主动提问澄清,通过大规模在线强化学习实现复杂任务自动化,在出行、办公等高频场景中表现卓越,已登顶ScreenSpot-Pro、MobileWorld等多项SOTA评测。
1555 7
|
4天前
|
Linux 数据库
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
本文介绍在CentOS 7.9环境下安装PolarDB-X单机版数据库的完整流程,涵盖系统环境准备、本地Yum源配置、RPM包安装、用户与目录初始化、依赖库解决、数据库启动及客户端连接等步骤,助您快速部署运行PolarDB-X。
252 1
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
|
9天前
|
人工智能 前端开发 API
Google发布50页AI Agent白皮书,老金帮你提炼10个核心要点
老金分享Google最新AI Agent指南:让AI从“动嘴”到“动手”。Agent=大脑(模型)+手(工具)+协调系统,可自主完成任务。通过ReAct模式、多Agent协作与RAG等技术,实现真正自动化。入门推荐LangChain,文末附开源知识库链接。
684 119