三種定時器:
·關於C#中timer類 在C#里關於定時器類就有3個
1、基於 Windows 的標准計時器(System.Windows.Forms.Timer)
2、基於服務器的計時器(System.Timers.Timer)
3、線程計時器(System.Threading.Timer)
System.Windows.Forms.Timer是應用於WinForm中的,它是通過Windows消息機制實現的,類似於VB或Delphi中 的Timer控件,內部使用API SetTimer實現的。它的主要缺點是計時不精確,而且必須有消息循環,Console Application(控制台應用程序)無法使用。
System.Timers.Timer和System.Threading.Timer非常類似,它們是通過.NET Thread Pool實現的,輕量,計時精確,對應用程序、消息沒有特別的要求。System.Timers.Timer還可以應用於WinForm,完全取代上面的 Timer控件。它們的缺點是不支持直接的拖放,需要手工編碼。
例:
使用System.Timers.Timer類
System.Timers.Timer t = new System.Timers.Timer(10000);//實例化Timer類,設置間隔時間為10000毫秒;
t.Elapsed += new System.Timers.ElapsedEventHandler(theout);//到達時間的時候執行事件;
t.AutoReset = true;//設置是執行一次(false)還是一直執行(true);
t.Enabled = true;//是否執行System.Timers.Timer.Elapsed事件;
public void theout(object source, System.Timers.ElapsedEventArgs e)
{
MessageBox.Show("OK!");
}
一、基於 Windows 的標准計時器(System.Windows.Forms.Timer)
微軟的注釋很明確:“實現按用戶定義的時間間隔引發事件的計時器。此計時器最宜用於 Windows 窗體應用程序中,並且必須在窗口中使用。”,在線程中使用,其相應的事件是不會觸發的。
首先注意一點就是:Windows 計時器是為單線程環境設計的,此計時器從Visual Basic 1.0 版起就存在於該產品中,並且基本上未做改動,這個計時器是使用最簡單的一種,只要把工具箱中的Timer控件拖到窗體上,然后設置一下事件和間隔時間等屬性就可以了,實驗出來的結果也完全符合單線程的特點:
1、當啟動此計時器后,會在下方子線程ID列表中顯示子線程ID,並且和主線程ID相同
private void formsTimer_Tick(object sender, EventArgs e) { i++; lblSubThread.Text += "子線程執行,線程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n"; }
2、當單擊主線程暫停5秒后,子線程會暫停執行,並且當5秒之后不會執行之前被暫停的子線程,而是直接執行后面的子線程(也就是會少輸出幾行值)
System.Threading.Thread.Sleep(5000);
3、在子進程的事件中暫停5秒會導致主窗口相應無響應5秒
4、定義一個線程靜態變量:
[ThreadStatic]
private static int i = 0;
在子線程事件中每次加一,再點擊線程靜態變量值會得到增加后的i值
二、基於服務器的計時器(System.Timers.Timer)
System.Timers.Timer不依賴窗體,是從線程池喚醒線程,是傳統的計時器為了在服務器環境上運行而優化后的更新版本,在VS2005的工具箱中沒有提供現成的控件,需要手工編碼使用此計時器。
使用方式有兩種,
1、通過SynchronizingObject屬性依附於窗體
System.Timers.Timer timersTimer = new System.Timers.Timer();
timersTimer.Enabled = false;
timersTimer.Interval = 100;
timersTimer.Elapsed += new System.Timers.ElapsedEventHandler(timersTimer_Elapsed);
timersTimer.SynchronizingObject = this;
通過這種方式來使用,實驗效果幾乎和基於 Windows 的標准計時器一樣,只是在上面的第二條實驗中,雖然也會暫停子線程的執行,不過在5秒之后把之前排隊的任務都執行掉(也就是不會少輸出幾行值)
2、不使用SynchronizingObject屬性
這種方式就是多線程的方式了,即啟動的子線程和主窗體不在一個線程。不過這樣也存在一個問題:由於子線程是單獨的一個線程,那么就不能訪問住窗體中的控件了,只能通過代理的方式來訪問:
delegate void SetTextCallback(string text);
void timersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { //使用代理 string text = "子線程執行,線程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n"; SetTextCallback d = new SetTextCallback(SetText); this.Invoke(d, new object[] { text }); i++; } private void SetText(string text) { lblSubThread.Text += text; }
這樣我們再次實驗就會得到如下的結果:
1、當啟動此計時器后,會在下方子線程ID列表中顯示子線程ID,並且和主線程ID不相同
2、當單擊主線程暫停5秒后,子線程會一直往下執行(界面上可能看不出來,不過通過在子線程輸出文件的方式可以很方便的看出來)
3、在子進程的事件中暫停5秒不會導致主窗口無響應
4、在子線程事件中每次給線程靜態變量加一,再點擊線程靜態變量值得到的值還是0(不會改變主窗口中的線程靜態變量)
三、線程計時器(System.Threading.Timer)
線程計時器也不依賴窗體,是一種簡單的、輕量級計時器,它使用回調方法而不是使用事件,並由線程池線程提供支持。
對消息不在線程上發送的方案中,線程計時器是非常有用的。
使用方法如下:
System.Threading.Timer threadTimer; public void ThreadMethod(Object state) { //使用代理 string text = "子線程執行,線程ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n"; SetTextCallback d = new SetTextCallback(SetText); this.Invoke(d, new object[] { text }); i++; } private void Form1_Load(object sender, EventArgs e) { threadTimer = new System.Threading.Timer(new System.Threading.TimerCallback(ThreadMethod), null, -1, -1); }
System.Threading.Timer 是由線程池調用的。所有的Timer對象只使用了一個線程來管理。這個線程知道下一個Timer對象在什么時候到期。下一個Timer對象到期時,線程就會喚醒,在內部調用ThreadPool 的 QueueUserWorkItem,將一個工作項添加到線程池隊列中,使你的回調方法得到調用。如果回調方法的執行時間很長,計時器可能(在上個回調還沒有完成的時候)再次觸發。這可能造成多個線程池線程同時執行你的回調方法。
周期到就會再次調用回調方法,而不管上次是否執行完成,形成多個線程池線程同時執行!
參數
callback : 一個Object 類型參數的委托,周期調用的函數。
state: callback 委托調用時的參數。
dueTime: 定時器延時多久開始調用。單位 毫秒
period: 定時器每隔多久調用一次。單位 毫秒
不能使用局部變量來創建指向一個線程定時器。因為局部變量會被GC回收,導致定時器失效。
比如下面的例子:
static void Main(string[] args) { Run(); //為了加開GC垃圾回收,不停的創建對象 for (int i = 0; i < 10000; i++) { cc tmp = new cc(); Thread.Sleep(10); } Console.ReadKey(); } static int id; static void Run() { Timer timer = new Timer(DoTime, null, 500, 500); } static void DoTime(object obj) { Console.WriteLine(id++); } class cc { public int[] a = new int[1000]; }
定時器執行4次后停止了。定時器什么時候停止取決於GC什么時候回收。如果一直沒有GC的回收,那么將會一直執行下去。比如把上方創建 cc 對象的代碼刪除。定時器將會一直執行。
可以在Main方法中使局部變量 或者 使用全局變量來存放Timer 對象 避免 clr 把Timer 回收。
static Timer timer; static void Main(string[] args) { timer = new Timer(DoTime, null, 500, 500); //為了加開GC垃圾回收,不停的創建對象 for (int i = 0; i < 10000; i++) { cc tmp = new cc(); Thread.Sleep(10); } Console.ReadKey(); } static int id; static void DoTime(object obj) { Console.WriteLine(id++); }
如果回調方法的執行時間很長,計時器可能(在上個回調還沒有完成的時候)再次觸發。這可能造成多個線程池線程同時執行你的回調方法。
static Timer timer1; static void Main(string[] args) { timer1 = new Timer(DoTime, 1, 500, 500); Console.ReadKey(); } static int id; static void DoTime(object obj) { int idx = id ++; Console.WriteLine("處理開始:" + idx + "," + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(1200); Console.WriteLine("處理完畢:" + idx + "," + Thread.CurrentThread.ManagedThreadId); }
定時器第一次任務還沒執行完畢,第二次,第三次回調就開始了。多個線程同時並發 DoTime 方法。
回調方法運行時,id的值被COPY到線程空間,不會被其它線程的操作改變,每個DoTime 方法都有自己的線程空間。
解決方法【取消周期調用】
period 【周期調用時間】使用 Timeout.Infinite。這個參數將導致DoTime 只處理一次。
在回調方法中使用 Change方法來修改 dueTime【延時時間】,period 【周期時間】參數。period 繼續使用 Timeout.Infinite. 使用這個方法要注意如果timer 在被Dispose了,使用Change 將會引發異常。
比如
static Timer timer1; static void Main(string[] args) { timer1 = new Timer(DoTime, 1, 0, Timeout.Infinite); Console.ReadKey(); } static int id; static void DoTime(object obj) { int idx = id ++; Console.WriteLine("處理開始:" + idx + "," + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(1200); Console.WriteLine("處理完畢:" + idx + "," + Thread.CurrentThread.ManagedThreadId); timer1.Change(500,Timeout.Infinite); //設置下次調用時間 }
使用Disponse停止定時器
如果Timer 不會再使用 則可以 使用 Dispose 方法來停止定時器。
如果定時器運行到中途,使用Dispose方法后,callback還是會執行完一個完整的生命周期,不會中途停止。並且Dispose方法不會等待 callback的這次調用完成。只是定時器下次不再調用 callback。
使用Change停止定時器
把 dueTime 參數置為-1就可以停止定時器。同樣的,它不會中斷在運行中的callback,只是下一次不再回調。 這個方法停止的定時器 還可以使用Change 再次利用定時器。
四、c#每隔一段時間執行代碼
方法一:調用線程執行方法,在方法中實現死循環,每個循環Sleep設定時間;
方法二:使用System.Timers.Timer類;
方法三:使用System.Threading.Timer;
using System; using System.Collections; using System.Threading; public class Test { public static void Main() { Test obj = new Test(); Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString()); //方法一:調用線程執行方法,在方法中實現死循環,每個循環Sleep設定時間 Thread thread = new Thread(new ThreadStart(obj.Method1)); thread.Start(); //方法二:使用System.Timers.Timer類 System.Timers.Timer t = new System.Timers.Timer(100);//實例化Timer類,設置時間間隔 t.Elapsed += new System.Timers.ElapsedEventHandler(obj.Method2);//到達時間的時候執行事件 t.AutoReset = true;//設置是執行一次(false)還是一直執行(true) t.Enabled = true;//是否執行System.Timers.Timer.Elapsed事件 while (true) { Console.WriteLine("test_" +Thread.CurrentThread.ManagedThreadId.ToString()); Thread.Sleep(100); } //方法三:使用System.Threading.Timer //Timer構造函數參數說明: //Callback:一個 TimerCallback 委托,表示要執行的方法。 //State:一個包含回調方法要使用的信息的對象,或者為空引用(Visual Basic 中為 Nothing)。 //dueTime:調用 callback 之前延遲的時間量(以毫秒為單位)。指定 Timeout.Infinite 以防止計時器開始計時。指定零 (0) 以立即啟動計時器。 //Period:調用 callback 的時間間隔(以毫秒為單位)。指定 Timeout.Infinite 可以禁用定期終止。 System.Threading.Timer threadTimer = new System.Threading.Timer(new System.Threading.TimerCallback(obj.Method3),null, 0, 100); while (true) { Console.WriteLine("test_" +Thread.CurrentThread.ManagedThreadId.ToString()); Thread.Sleep(100); } Console.ReadLine(); } void Method1() { while (true) { Console.WriteLine(DateTime.Now.ToString()+ "_" + Thread.CurrentThread.ManagedThreadId.ToString()); Thread.CurrentThread.Join(100);//阻止設定時間 } } void Method2(object source,System.Timers.ElapsedEventArgs e) { Console.WriteLine(DateTime.Now.ToString()+ "_" + Thread.CurrentThread.ManagedThreadId.ToString()); } void Method3(Objectstate) { Console.WriteLine(DateTime.Now.ToString()+ "_" +Thread.CurrentThread.ManagedThreadId.ToString()); } }