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