再C#裡現在有3個Timer類:
- System.Windows.Forms.Timer
- System.Threading.Timer
- System.Timers.Timer
這三個Timer我想大家對System.Windows.Forms.Timer已經很熟悉了,唯一我要說的就是這個Timer在激發Timer.Tick事件的時候,事件的處理函數是在程序主線程上執行的,所以在WinForm上面用這個Timer很方便,因為在From上的所有控件都是在程序主線程上創建的,那麼在Tick的處理函數中可以對Form上的所有控件進行操作,不會造成WinForm控件的線程安全問題。
1、Timer運行的核心都是System.Threading.ThreadPool
在這裡要提到ThreadPool(線程池)是因為,System.Threading.Timer 和System.Timers.Timer運行的核心都是線程池,Timer每到間隔時間後就會激發響應事件,因此要申請線程來執行對應的響應函數,Timer將獲取線程的工作都交給了線程池來管理,每到一定的時間後它就去告訴線程池:“我現在激發了個事件要運行對應的響應函數,麻煩你給我向操作系統要個線程,申請交給你了,線程分配下來了你就運行我給你的響應函數,沒分配下來先讓響應函數在這兒排隊(操作系統線程等待隊列)”,消息已經傳遞給線程池了,Timer也就不管了,因為它還有其他的事要做(每隔一段時間它又要激發事件),至於提交的請求什麼時候能夠得到滿足,要看線程池當前的狀態:
- 1、如果線程池現在有線程可用,那麼申請馬上就可以得到滿足,有線程可用又可以分為兩種情況:
- <1>線程池現在有空閒線程,現在馬上就可以用
- <2>線程池本來現在沒有線程了,但是剛好申請到達的時候,有線程運行完畢釋放了,那麼申請就可以用別人釋放的線程。
- 這兩種情況情況就如同你去游樂園玩賽車,如果游樂園有10輛車,現在有3個人在玩,那麼還剩7輛車,你去了當然可以選一輛開。另外還有一種情況就是你到達游樂園前10輛車都在開,但是你運氣很好,剛到游樂園就有人不玩了,正好你坐上去就可以接著開。
- 2、如果現在線程池現在沒有線程可用,也分為兩種情況:
- <1>線程池現有線程數沒有達到設置的最大工作線程數,那麼隔半秒鐘.net framework就會向操作系統申請一個新的線程(為避免向線程分配不必要的堆棧空間,線程池按照一定的時間間隔創建新的空閒線程。該時間間隔目前為半秒,但它在 .NET Framework 的以後版本中可能會更改)。
- <2>線程池現有工作線程數達到了設置的最大工作線程數,那麼申請只有在等待隊列一直等下去,直到有線程執行完任務後被釋放。
那麼上面提到了線程池有最大工作線程數,其實還有最小空閒線程數,那麼這兩個關鍵字是什麼意思呢:
- 1、最大工作線程數:實際上就是指的線程池能夠向操作系統申請的最大線程數,這個值在.net framework中有默認值,這個默認值是根據你計算機的配置來的,當人你可以用ThreadPool.GetMaxThreads返回線程池當前最大工作線程數,你也可以同ThreadPool.SetMaxThreads設置線程池當前最大工作線程數。
- 2、最小空閒線程數:是指在程序開始後,線程池就默認向操作系統要最小空閒線程數個線程,另外這也是線程池維護的空閒線程數(如果線程池最小空閒線程數為3,當前因為一些線程執行完任務被釋放,線程池現在實際上有10個空閒線程,那麼線程池會讓操作系統釋放多余的7個線程,而只維持3個空閒線程供程序使用),因為上面說了,在執行程序的時候在要線程池申請線程有半秒的延遲時間,這也會影響程序的性能,所以把握好這個值很重要,用樣你可以用ThreadPool.GetMinThreads返回線程池當前最小空閒線程數,你也可以同ThreadPool.SetMinThreads設置線程池當前最小空閒線程數。
下面是我給的例子,這個例子讓線程池申請800個線程,其中設置最大工作線程數為500,800個線程任務每個都要執行100000000毫秒目的是讓線程不會釋放,並且讓用戶選擇,是否預先申請500個空閒線程免受那半秒鐘的延遲時間,其結果可想而知當線程申請到500的時候,線程池達到了最大工作線程數,剩余的300個申請進入漫長的等待
2、System.Threading.Timer
談完了線程池,就可以開始討論Timer,這裡我們先從System.Threading.Timer開始,System.Threading.Timer的作用就是每到間隔時間後激發響應事件並執行相應函數,執行響應函數要向線程池申請線程,當然申請中會遇到一些情況在上面我們已經說了。值得注意的一點就是System.Threading.Timer在創建對象後立即開始執行,比如System.Threading.Timer timer = new System.Threading.Timer(Excute, null, 0, 10);這句執行完後每隔10毫秒就執行Excute函數不需要啟動什麼的。下面就舉個例子,我先把代碼貼出來:
代碼
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
namespace ConsoleApplication1
{
class UnSafeTimer
{
static int i = 0;
static System.Threading.Timer timer;
static object mylock = new object();
static int sleep;
static bool flag;
public static Stopwatch sw = new Stopwatch();
static void Excute(object obj)
{
Thread.CurrentThread.IsBackground = false;
int c;
lock (mylock)
{
i++;
c = i;
}
if (c == 80)
{
timer.Dispose();//執行Dispose後Timer就不會再申請新的線程了,但是還是會給Timmer已經激發的事件申請線程
sw.Stop();
}
if (c < 80)
Console.WriteLine("Now:" + c.ToString());
else
{
Console.WriteLine("Now:" + c.ToString()+"-----------Timer已經Dispose耗時:"+sw.ElapsedMilliseconds.ToString()+"毫秒");
}
if (flag)
{
Thread.Sleep(sleep);//模擬花時間的代碼
}
else
{
if(i<=80)
Thread.Sleep(sleep);//前80次模擬花時間的代碼
}
}
public static void Init(int p_sleep,bool p_flag)
{
sleep = p_sleep;
flag = p_flag;
timer = new System.Threading.Timer(Excute, null, 0, 10);
}
}
class SafeTimer
{
static int i = 0;
static System.Threading.Timer timer;
static bool flag = true;
static object mylock = new object();
static void Excute(object obj)
{
Thread.CurrentThread.IsBackground = false;
lock (mylock)
{
if (!flag)
{
return;
}
i++;
if (i == 80)
{
timer.Dispose();
flag = false;
}
Console.WriteLine("Now:" + i.ToString());
}
Thread.Sleep(1000);//模擬花時間的代碼
}
public static void Init()
{
timer = new System.Threading.Timer(Excute, null, 0, 10);
}
}
class Program
{
static void Main(string[] args)
{
Console.Write("是否使用安全方法(Y/N)?");
string key = Console.ReadLine();
if (key.ToLower() == "y")
SafeTimer.Init();
else
{
Console.Write("請輸入Timmer響應事件的等待時間(毫秒):");//這個時間直接決定了前80個任務的執行時間,因為等待時間越短,每個任務就可以越快執行完,那麼80個任務中就有越多的任務可以用到前面任務執行完後釋放掉的線程,也就有越多的任務不必去線程池申請新的線程避免多等待半秒鐘的申請時間
string sleep = Console.ReadLine();
Console.Write("申請了80個線程後Timer剩余激發的線程請求是否需要等待時間(Y/N)?");//這裡可以發現選Y或者N只要等待時間不變,最終Timer激發線程的次數都相近,說明Timer的確在執行80次的Dispose後就不再激發新的線程了
key = Console.ReadLine();
bool flag = false;
if (key.ToLower() == "y")
{
flag = true;
}
UnSafeTimer.sw.Start();
UnSafeTimer.Init(Convert.ToInt32(sleep), flag);
}
Console.ReadLine();
}
}
}
這個例子包含了兩個Timer的類UnSafeTimer和SafeTimer,兩個類的代碼的大致意思就是使用Timer每隔10毫秒就執行Excute函數,Excute函數會顯示當前執行的次數,在80次的時候通過timer.Dispose()讓Timer停止不再激發響應事件。
首先我們來分析下UnSafeTimer
class UnSafeTimer
{
static int i = 0;
static System.Threading.Timer timer;
static object mylock = new object();
static int sleep;
static bool flag;
public static Stopwatch sw = new Stopwatch();
static void Excute(object obj)
{
Thread.CurrentThread.IsBackground = false;
int c;
lock (mylock)
{
i++;
c = i;
}
if (c == 80)
{
timer.Dispose();//執行Dispose後Timer就不會再申請新的線程了,但是還是會給Timmer已經激發的事件申請線程
sw.Stop();
}
if (c < 80)
Console.WriteLine("Now:" + c.ToString());
else
{
Console.WriteLine("Now:" + c.ToString() + "-----------Timer已經Dispose耗時:" + sw.ElapsedMilliseconds.ToString() + "毫秒");
}
if (flag)
{
Thread.Sleep(sleep);//模擬花時間的代碼
}
else
{
if (i <= 80)
Thread.Sleep(sleep);//前80次模擬花時間的代碼
}
}
public static void Init(int p_sleep, bool p_flag)
{
sleep = p_sleep;
flag = p_flag;
timer = new System.Threading.Timer(Excute, null, 0, 10);
}
}
你可以執行試一試,在輸入是否執行安全方法的時候選N,等待時間1000,申請了80個線程後Timer剩余激發的線程選N,本來想在80次的時候停下來,可是你會發現直到執行到660多次之後才停下來(具體看機器配置),申請前80個線程的時間為10532毫秒,反正執行的次數大大超出了限制的80次,回頭想想讓Timer不在激發事件的方法是調用timer.Dispose(),難不成是Dispose有延遲?延遲的過程中多執行了500多次?那麼我們再來做個試驗,我們在申請了80個線程後Timer剩余激發的線程選y,請耐心等待結果,在最後你會發現執行時間還是660次左右,這很顯然是不合理的,如果Dispose有延遲時間造成所執行500多次,那麼加長80次後面每個線程的申請時間在相同的延遲時間內申請的線程數應該減少,因為後面500多個線程每個線程都要執行1000毫秒,那麼勢必有些線程會去申請新的線程有半秒鐘的等待時間(你會發現申請了80個線程後Timer剩余激發的線程選y明顯比選n慢得多,就是因為這個原因),所以看來不是因為Dispose造成的。
那麼會是什麼呢?我們這次這樣選在輸入是否執行安全方法的時候選N,等待時間500,申請了80個線程後Timer剩余激發的線程選N
那麼會是什麼呢?我們這次這樣選在輸入是否執行安全方法的時候選N,等待時間50,申請了80個線程後Timer剩余激發的線程選N
我們發現隨著每次任務等待時間的減少多執行的次數也在減少,最關鍵的一點我們從圖中可以看到,前80次任務申請的時間也在減少,這是最關鍵的,根據上面線程池所講的內容我們可以歸納出:每次任務的等待時間直接決定了前80個任務的執行時間,因為等待時間越短,每個任務就可以越快執行完,那麼80個任務中就有越多的任務可以用到前面任務執行完後釋放掉的線程,也就有越多的任務不必去線程池申請新的線程避免多等待半秒鐘的申請時間,而Timer並不會去關心線程池申請前80個任務的時間長短,只要它沒有執行到timer.Dispose(),它就會每隔10毫秒激發一次響應時間,不管前80次任務執行時間是長還是短,timer都在第80次任務才執行Dispose,執行Dispose後timer就不會激發新的事件了,但是如果前80次任務申請的時間越長,那麼timer就會在前80次任務申請的時間內激發越多響應事件,那麼線程池中等待隊列中就會有越多的響應函數等待申請線程,System.Threading.Timer沒有機制取消線程池等待隊列中多余的申請數,所以導致等待時間越長,80次後執行的任務數越多。
由此只用timer.Dispose()來終止Timer激發事件是不安全的,所以又寫了個安全的執行機制:
class SafeTimer
{
static int i = 0;
static System.Threading.Timer timer;
static bool flag = true;
static object mylock = new object();
static void Excute(object obj)
{
Thread.CurrentThread.IsBackground = false;
lock (mylock)
{
if (!flag)
{
return;
}
i++;
if (i == 80)
{
timer.Dispose();
flag = false;
}
Console.WriteLine("Now:" + i.ToString());
}
Thread.Sleep(1000);//模擬花時間的代碼
}
public static void Init()
{
timer = new System.Threading.Timer(Excute, null, 0, 10);
}
}
安全類中我們用了個bool類型的變量flag來判斷當前是否執行到80次了,執行到80次後將flag置為false,然後timer.Dispose,這時雖然任務還是要多執行很多次但是由於flag為false,Excute函數一開始就做了判斷flag為false會立即退出,Excute函數80次後相當於就不執行了。
3、System.Timers.Timer
在上面的例子中我們看到System.Threading.Timer很不安全,即使在安全的方法類,也只能讓事件響應函數在80次後立刻退出讓其執行時間近似於0,但是還是浪費了系統不少的資源。
所以本人更推薦使用現在介紹的System.Timers.Timer,System.Timers.Timer大致原理和System.Threading.Timer差不多,唯一幾處不同的就是:
- 構造函數不同,構造函數可以什麼事情也不做,也可以傳入響應間隔時間:System.Timers.Timer timer = new System.Timers.Timer(10);
- 響應事件的響應函數不在構造函數中設置:timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
- 聲明System.Timers.Timer對象後他不會自動執行,需要調用 timer.Start()或者timer.Enabled = true來啟動它, timer.Start()的內部原理還是設置timer.Enabled = true
- 調用 timer.Stop()或者timer.Enabled = false來停止引發Elapsed事件, timer.Stop()的內部原理還是設置timer.Enabled = false,最重要的是timer.Enabled = false後會取消線程池中當前等待隊列中剩余任務的執行。
那麼我們來看個例子:
代碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using System.Threading;
namespace ConsoleApplication2
{
class UnSafeTimer
{
static int i = 0;
static System.Timers.Timer timer;
static object mylock = new object();
public static void Init()
{
timer = new System.Timers.Timer(10);
timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
timer.Start();
}
static void timer_Elapsed(object sender, ElapsedEventArgs e)
{
Thread.CurrentThread.IsBackground = false;
int c;
lock (mylock)
{
i++;
c = i;
}
Console.WriteLine("Now:" + i.ToString());
if (c == 80)
{
timer.Stop();//可應看到System.Timers.Timer的叫停機制比System.Threading.Timer好得多,就算在不安全的代碼下Timer也最多多執行一兩次(我在試驗中發現有時會執行到81或82),說明Stop方法在設置Timer的Enable為false後不僅讓Timer不再激發響應事件,還取消了線程池等待隊列中等待獲得線程的任務,至於那多執行的一兩次任務我個人認為是Stop執行過程中會耗費一段時間才將Timer的Enable設置為false,這段時間多余的一兩個任務就獲得了線程開始執行
}
Thread.Sleep(1000);//等待1000毫秒模擬花時間的代碼,注意:這裡的等待時間直接決定了80(由於是不安全模式有時會是81或82、83)個任務的執行時間,因為等待時間越短,每個任務就可以越快執行完,那麼80個任務中就有越多的任務可以用到前面任務執行完後釋放掉的線程,也就有越多的任務不必去線程池申請新的線程避免多等待半秒鐘的申請時間
}
}
class SafeTimer
{
static int i = 0;
static System.Timers.Timer timer;
static bool flag = true;
static object mylock = new object();
public static void Init()
{
timer = new System.Timers.Timer(10);
timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
timer.Start();
}
static void timer_Elapsed(object sender, ElapsedEventArgs e)
{
Thread.CurrentThread.IsBackground = false;
lock (mylock)
{
if (!flag)
{
return;
}
i++;
Console.WriteLine("Now:" + i.ToString());
if (i == 80)
{
timer.Stop();
flag = false;
}
}
Thread.Sleep(1000);//同UnSafeTimer
}
class Program
{
static void Main(string[] args)
{
Console.Write("是否使用安全Timer>(Y/N)?");
string Key = Console.ReadLine();
if (Key.ToLower() == "y")
SafeTimer.Init();
else
UnSafeTimer.Init();
Console.ReadLine();
}
}
}
}
這個例子和System.Threading.Timer差不多,這裡也分為:安全類SafeTimer和不安全類UnSafeTimer,原因是 timer.Stop()有少許的延遲時間有時任務會執行到81~83,但是就算是不安全方法也就最多多執行幾次,不像System.Threading.Timer多執行上百次...
所以我這裡還是推薦大家使用System.Timers.Timer
可是CLR via C#說最好不要用System.Timers.Timer,具體原因我也沒有看很明白,博主你看看,分析一下
恩,CLR Via C#這個是肯定是權威的解釋,但是實際測試下來,的確System.Timers.Timer的精確度要比System.Threading.Timer,但是如果其核心還是System.Threading.Timer的話,那麼使用System.Threading.Timer應該是更合適的。