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就是他的語法糖.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM