zoukankan      html  css  js  c++  java
  • C# 多线程九之Timer类

    1、简介

    //
            // 摘要:
            //     使用 32 位的有符号整数指定时间间隔,初始化 Timer 类的新实例。
            //
            // 参数:
            //   callback:
            //     一个 System.Threading.TimerCallback 委托,表示要执行的方法。
            //
            //   state:
            //     一个包含回调方法要使用的信息的对象,或者为 null。
            //
            //   dueTime:
            //     调用 callback 之前延迟的时间量(以毫秒为单位)。指定 System.Threading.Timeout.Infinite 可防止启动计时器。指定零
            //     (0) 可立即启动计时器。
            //
            //   period:
            //     调用 callback 的时间间隔(以毫秒为单位)。指定 System.Threading.Timeout.Infinite 可以禁用定期终止。
            //
            // 异常:
            //   T:System.ArgumentOutOfRangeException:
            //     dueTime 或 period 参数为负,并且不等于 System.Threading.Timeout.Infinite。
            //
            //   T:System.ArgumentNullException:
            //     callback 参数为 null。
            [SecuritySafeCritical]
            public Timer(TimerCallback callback, object state, int dueTime, int period);

    相信写过定时任务的小伙伴都知道这个类,非常的轻量级,而且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就是他的语法糖.

  • 相关阅读:
    软件体系架构复习要点
    Operating System on Raspberry Pi 3b
    2019-2020 ICPC North-Western Russia Regional Contest
    2019 ICPC ShenYang Regional Online Contest
    2019 ICPC XuZhou Regional Online Contest
    2019 ICPC NanChang Regional Online Contest
    2019 ICPC NanJing Regional Online Contest
    Codeforces Edu Round 72 (Rated for Div. 2)
    Codeforces Round #583 (Div.1+Div.2)
    AtCoder Beginning Contest 139
  • 原文地址:https://www.cnblogs.com/grj001/p/12223175.html
Copyright © 2011-2022 走看看