专栏首页GreenLeavesC# 多线程九之Timer类

C# 多线程九之Timer类

1、简介

相信写过定时任务的小伙伴都知道这个类,非常的轻量级,而且FCL中大量的类使用了这个方法,比如CancellationTokenSource的CancelAfter就是用Timer去做的.

当然FCL中大量的使用了Timer,说明MS对Timer类是信任的.下面就开始介绍这个类的用法.简介很少,但是很有力,FCL中都用了这么多,所以我们不应该带有色眼镜看它.当然它也不是万能的,要不然就不会出现那么多的定时任务项目了.

Timer的本质:当计时器档期,CLR会将我们的回调函数放入到线程池队列中,并执行我们的回调函数.仅此而已.下面会演示

2、基本用法

使用 System.Threading.Timer前,你必须知道它是基于线程池线程的,其实,Timer的作用是定时(可以是一个时间点,可以试一段时间)调用一个方法,但是他是怎么做的呢?其实当你在你的代码中创建了一个或多个Timer实例时,线程池会给每个的Timer实例分配一个线程,代码如下:

        static void Main(string[] args)
        {
            var timer = new Timer(state =>
            {
                Console.WriteLine("每秒执行一次的定时任务,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            }, null, 0, 1000);

            var timer2 = new Timer(state =>
            {
                Console.WriteLine("每秒执行一次的定时任务,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            }, null, 0, 1000);
            Console.ReadKey();
        }

两个定时任务,分配了三个线程,很奇怪,我还以为只会给一个Timer实例分配一个线程,但事实并不是.那么证明当一个timer当期时,线程池就会唤起一个空闲的线程去执行回调函数.如果你把间隔的时间改长,如下:

        static void Main(string[] args)
        {
            var timer = new Timer(state =>
            {
                Console.WriteLine("每秒执行一次的定时任务,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            }, null, 0, 3000);

            var timer2 = new Timer(state =>
            {
                Console.WriteLine("每秒执行一次的定时任务,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            }, null, 0, 3000);
            Console.ReadKey();
        }

只会唤起两个线程.

 如果把时间改的非常小,如下:

        static void Main(string[] args)
        {
            var timer = new Timer(state =>
            {
                Console.WriteLine("每秒执行一次的定时任务,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            }, null, 0, 10);

            var timer2 = new Timer(state =>
            {
                Console.WriteLine("每秒执行一次的定时任务,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            }, null, 0, 10);
            Console.ReadKey();
        }

回唤起更多的线程参与运算,综上所述每个回调方法线程池会给它分配一个线程,到底会分配多少个线程取决于你定的间隔时间.

3、里面的坑

(1)、线程安全问题

有了上面的实践,所以当你需要给Timer传递共享的参数时,必须要考虑线程安全问题,要不然就会像下面这样:

        static void Main(string[] args)
        {
            var totalCount = 0;
            var param = 0;
            var timer2 = new Timer(state =>
            {
                //线程安全的加法操作
                Interlocked.Add(ref totalCount, param++);
                //不安全的操作
                param = param++;
                Console.WriteLine("每秒执行一次的定时任务,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            }, null, 0, 10);
            Console.ReadKey();
        }

so,你懂的,使用Timer要注意线程安全问题.

(2)、回调函数的执行时间大于给Timer实例设置的时间间隔

        static object lockObj = new object();
        static void Main(string[] args)
        {
            var count = 0;
            var timer2 = new Timer(state =>
            {
                lock (lockObj)
                {
                    count++;
                }
                //如果线程池会等待该方法执行完毕,那么6秒后会输出2;
                Console.WriteLine(count);
                Thread.Sleep(3000);
            }, null,0, 500);
            Console.ReadKey();
        }

事实证明不是,需要你自己去跑下上面这段代码,总之Timer并没有等待回调函数执行完毕,而是没过500毫秒唤起一个线程执行+1操作.导致了多个线程池执行了这个回调方法.

那么如何解决这个问题呢?如下:

    class Program
    {
        private static Timer _timer;
        static object lockObj = new object();
        static void Main(string[] args)
        {
            var count = 0;
             //创建但并不启动计时器
             _timer = new Timer(obj=> {
                 Console.WriteLine("开始执行的当前秒数:{0},当前线程Id:{1}", DateTime.Now.Second,Thread.CurrentThread.ManagedThreadId);
                lock (lockObj)
                {
                    count++;
                }
                Console.WriteLine(count);
                Thread.Sleep(3000);
                 //当前线程执行加1操作完毕后,让Timer在500毫秒后再次触发
                _timer.Change(0, Timeout.Infinite);
                 Console.WriteLine("执行完毕后的当前秒数:{0},当前线程Id:{1}", DateTime.Now.Second, Thread.CurrentThread.ManagedThreadId);
             },null,Timeout.Infinite,Timeout.Infinite);

            //启动计时器
            _timer.Change(0, Timeout.Infinite);

            Console.ReadKey();
        }
    }

所以,当你的计算任务过于复杂你无法判断它多久才会执行完毕时,上面这种做法才是最好的做法.当Timer处理完一个回调函数之后,在回调函数内部调用Change方法,重启它,这样就保证你当前执行的计算任务只会有一个线程进行调用.而不是向(1)中的那样,注意线程池不会等待上一个计算任务计算完毕之后开启一个新的timer.

(3)、时间间隔的不准确

这里不多做介绍,应为每次线程池和执行方法本身也会消耗时间,所以他的时间间隔想想都知道不是精确的.

 (4)、使用async await模型搭配Task.Delay实现定时任务

        static void Main(string[] args)
        {
            var timer = new Timer(obj => TimingOne(), null, 0, 6000);
            Console.ReadKey();
        }

        /// <summary>
        /// 使用async await模型搭配Task.Delay实现定时任务
        /// </summary>
        static async void TimingOne()
        {
            Console.WriteLine("循环任务一开启,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000);//开启一个守护线程,强制等待2秒后,执行后面的回调方法,也可以用Task的ContineWith实现
            TimingTwo();
        }

        static async void TimingTwo()
        {
            Console.WriteLine("循环任务二开启,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000);
            TimingThree();
        }

        static async void TimingThree()
        {
            Console.WriteLine("循环任务三开启,当前线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000);
        }

缺点不多说,你必须控制好时间,如果你的计算任务的时间不确定,不建议用这种方式,而且这里也可以使用Task.ContinueWith来实现,这里就不说了,因为async和await就是他的语法糖.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • C#核编之System.Console类

          顾名思义,Console类封装了基于控制台的输入输出和错误流的操作,下面列举一些System.Console类常用的成员的,这些成员能为简单的命令行...

    郑小超.
  • C# 多线程系列之异步回调(委托)

    本文参考自C#基础:线程之异步回调(委托),纯属读书笔记 在解析异步回调之前,先看同步回调的执行过程,以及代码原理。 1、线程的同步执行 同步执行:在主线程执...

    郑小超.
  • C#核编之X++详解

    重点:当X++单独使用时,就是没有其他符号参与运算,这时X做自增运算,而当X++与其他运算符一起参与运算时,这时的X++因为运算优先级低,所以是最后一个参与运算...

    郑小超.
  • 语法基础-方案二:C#阶段项目

    项目名称:制作一款窗口程序的飞行棋项目 项目需求:要求至少两人对战 开发周期:两天

    雷潮
  • CSharp基础知识3-循环语句

    py3study
  • 介绍一个神奇的命令rwho

    这个命令主要是来查看局域网中,记住是局域网所有安装了rwho的机器的启动时间和登录用户,这个命令的原理就是由rwho的rwhod守护进程每三分钟广播一次状态信息...

    bboysoul
  • PostgreSQL 的JSON 处理甩“你”几条街

    首先这里的你绝对不是MONGODB ,至于是谁,你是谁,那的先了解POSTGRESQL 处理 JSON 的方式后,才能确定那个你是谁。

    AustinDatabases
  • Java:并发不易,先学会用

    我从事Java编程已经11年了,绝对是个老兵;但对于Java并发编程,我只能算是个新兵蛋子。我说这话估计要遭到某些高手的冷嘲热讽,但我并不感到害怕。

    沉默王二
  • C# 多态性

    多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。

    landv
  • C#基础知识之方法重载总结

    方法重载是指在同一个类中方法同名,参数不同,调用时根据实参的形式,选择与他匹配的方法执行操作的一种技术。

    跟着阿笨一起玩NET

扫码关注云+社区

领取腾讯云代金券