談談C#中各種執行緒的使用及注意項~

說到執行緒,很多人會想到timer吧,

接下來我們就來學習一下 timer 吧,搖搖腦袋,清醒一下,接下來開始學習….

一、基本概念

1、什麼是進程? 
       當一個程式開始運行時,它就是一個進程,進程包括運行中的程式和程式所使用到的記憶體和系統資源。一個進程至少有一個主執行緒。

2、什麼是執行緒? 
       執行緒是程式中的一個執行流,每個執行緒都有自己的專有暫存器(棧指針、程式計數器等),但程式碼區是共享的,即不同的執行緒可以執行同樣的函數

3、什麼是多執行緒? 
       多執行緒是指程式中包含多個執行流,即在一個程式中可以同時運行多個不同的執行緒來執行不同的任務,也就是說允許單個程式創建多個並行執行的執行緒來完成各自的任務。

4、多執行緒的好處? 
       可以提高 CPU 的利用率。在多執行緒程式中,一個執行緒必須等待的時候,CPU 可以運行其它的執行緒而不是等待,這樣就大大提高了程式的效率。

5、多執行緒的不利方面? 
       執行緒也是程式,所以執行緒需要佔用記憶體,執行緒越多佔用記憶體也越多。 
       多執行緒需要協調和管理,所以 CPU 需要花時間來跟蹤執行緒。 
       執行緒之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題。 
       執行緒太多會導致控制太複雜,最終可能造成很多 Bug。

static void Main(string[] args)
{
    Thread.CurrentThread.Name = "It's Main Thread";
    Console.WriteLine(Thread.CurrentThread.Name + " [Status:" + Thread.CurrentThread.ThreadState + "]");
}

通過 Thread 類的靜態屬性 CurrentThread 可以獲取當前正在執行的執行緒。不管創建了多少個這個類的實例,但是類的靜態屬性在記憶體中只有一個。很容易理解 CurrentThread 為什麼是靜態的–雖然有多個執行緒同時存在,但是在某一個時刻,CPU 只能執行其中一個! 
       在.net framework class library 中,所有與多執行緒機制應用相關的類都是放在 System.Threading 命名空間中。

操縱一個執行緒

static void Main(string[] args)
{
    Console.WriteLine("Thread Start/Stop/Join Sample:");
    
    // 創建一個執行緒,使之執行 Beta 方法 
    Thread oThread = new Thread(Beta);
 
    // 實際上,Start 方法只是通知 CPU 此執行緒可以被執行,但具體執行時機則由 CPU 自行決定。
    oThread.Start();
    while (!oThread.IsAlive)
    {
        Thread.Sleep(1);
    }
 
    oThread.Abort();
    oThread.Join();
    Console.WriteLine();
    Console.WriteLine("Beta has finished");
    try
    {
        Console.WriteLine("Try to restart the Alpha.Beta thread");
        oThread.Start();
    }
    catch (ThreadStateException)
    {
        Console.WriteLine("ThreadStateException trying to restart Alpha.Beta. ");
        Console.WriteLine("Expected since aborted threads cannot be restarted.");
        Console.ReadLine();
    }
}
 
public static void Beta()
{
    while (true)
    {
        Console.WriteLine("Beta is running in its own thread.");
    }
}

 試圖用 Thread.Start() 方法重新啟動執行緒 oThread,但顯然 Abort() 方法帶來的後果是不可恢復的終止執行緒,所以最後程式會拋出 ThreadStateException 異常。

二、執行緒的優先順序

1、當執行緒之間爭奪 CPU 時,CPU 按照執行緒的優先順序給予服務。

2、在 C# 應用程式中,用戶可以設定 5 個不同的優先順序,由高到低分別是 HighestAboveNormalNormalBelowNormalLowest在創建執行緒時如果不指定優先順序,那麼系統默認為 ThreadPriority.Normal。 
       通過設定執行緒的優先順序,我們可以安排一些相對重要的執行緒優先執行,例如對用戶的響應等等。

static void Main(string[] args)
{
    Thread t1 = new Thread(() =>
    {
        System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
        watch.Start();
        for (int i = 0; i < 20000; i++)
        {
            // 模擬耗時工作
            var obj = new { name = "XXX", age = 37 };
            GC.Collect();
        }
        watch.Stop();
        Console.WriteLine("t1 finished[ {0} ]", watch.ElapsedMilliseconds);
    });
 
    Thread t2 = new Thread(() =>
    {
        System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
        watch.Start();
        for (int i = 0; i < 20000; i++)
        {
            var obj = new { name = "XXX", age = 37 };
            GC.Collect();
        }
        watch.Stop();
        Console.WriteLine("t2 finished[ {0} ]", watch.ElapsedMilliseconds);
    });
    t1.Priority = ThreadPriority.AboveNormal;
    t2.Priority = ThreadPriority.BelowNormal;
    t1.Start();
    t2.Start();
}

 執行緒的調度演算法非常複雜。記住,優先順序高的執行緒並不一定先執行,但 CPU 會將更多的時間片分給優先順序高的執行緒,因此,在相同任務量的前提下,高優先順序執行緒將會較快的完成任務。

三、Winform 中多執行緒的應用

1、在 Winform 程式中,一般負責控制UI介面的顯示、更新和控制項交互的執行緒為主執行緒,或 UI 執行緒。

2、單執行緒最顯著的缺點是,當一個事件發生,程式進行一個耗時的運算動作時,UI 執行緒會出現假死現象,此時會無視對用戶的響應。

下面的程式碼會模擬一些不同的情況:

void DoSomething()
{
    for (int i = 0; i < 900000000; i++)
    {
 
    }
    MessageBox.Show("It's finished.");
}
 
void ShowStr(object obj)
{
    var list = obj as List<string>;
    if (list != null)
    {
        foreach (var item in list)
        {
            MessageBox.Show(item.ToString());
        }                
    }
    else
        MessageBox.Show("null");
}
 
// UI 單執行緒,運行時窗體會卡死一段時間
private void btnUI_Click(object sender, EventArgs e)
{
    DoSomething();
}
 
// 調用無參函數,此時窗體能響應用戶
private void btnThreadA_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(DoSomething);
    thread.Start();
}
 
// 當所有前台執行緒都關閉時,後台執行緒將立即結束運行,無條件的關閉
// 而前台執行緒運行時,即使關閉 Form 主程式,該執行緒仍將繼續運行,直到計算完畢
private void btnThreadB_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(DoSomething);
    thread.IsBackground = true;
    thread.Start();
}
 
// 調用有參函數
private void btnThreadC_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(ShowStr);
    thread.Start(new List<string> { "Jacky", "Skysoot", "Sam" });
}

  要注意的是,執行緒在調用有參函數時,通過 Start() 方法傳遞了參數給指定委託,該委託又將參數傳遞給了該執行緒欲運行的函數。看微軟 Thread 類定義的元數據:

 

 Thread 類的 4 個構造函數基本分為 2 類,有參和無參。而 ParameterizedThreadStart 委託定義的方法原型的參數為 Object 類型,這提高了傳參最大的靈活性。當然,在被調用的函數內部,需要依據一定的約定將 Object 對象進行轉型處理。

封裝一個執行緒類進行函數和參數的傳遞

// 可定義各類型委託 示例暫定一個
public delegate void Do(object obj);
 
public class Worker
{
    Do method;
    object obj;
 
    private void Work()
    {
        method(obj);
    }
 
    // 創建工人執行緒時 new 出工人實例 並在執行緒上指定 Work()
    public static Thread CreateWorkerThread(Do method, object obj)
    {
        Worker worker = new Worker();
        worker.method = method;
        worker.obj = obj;
        Thread t = new Thread(worker.Work);
        return t;
    }
}
 
// 任務類
public class Quest
{
    public static void Quest1(object obj)
    {
        Console.WriteLine("工人開始:" + obj.ToString() + "\r\n");
    }
 
    public static void Quest2(object obj)
    {
        string[] list = obj as string[];
        if (obj != null)
        {
            foreach (var item in list)
            {
                Console.WriteLine("工人開始:" + item);
            }
        }
    }
}
 
public class Test
{
    public static void Main(string[] args)
    {
        Thread t1 = Worker.CreateWorkerThread(Quest.Quest1, "搬磚");
        t1.Start();
        Thread t2 = Worker.CreateWorkerThread(Quest.Quest2, new string[] {"聽音樂", "加班", "打撞球" });
        t2.Start();
    }
}

這種封裝只是一種啟發的方式而已,並非模式。但封裝委託後的好處在於,調用方可以靈活指定 Worker 類執行什麼類型的任務,加工什麼參數,而無需再去考慮其餘事情。

四、C#中timer類的用法

1、System.Windows.Forms.Timer  

實現按用戶定義的時間間隔引發事件的計時器。此計時器最宜用於 Windows 窗體應用程式中,並且必須在窗口中使用。

2、System.Threading.Timer  

提供以指定的時間間隔執行方法的機制。無法繼承此類。

3、System.Timers.Timer  

在應用程式中生成定期事件。 

這三個定時器位於不同的命名空間內,上面大概介紹了3個定時器的用途,其中第一個是只能在Windows窗體中使用的控制項。

在.NET1.1裡面,第3個System.Timers.Timer,也是可以拖拽使用,而.NET2.0開始取消了,只能手動編寫程式碼。而後2個沒有限制制。

下面通過具體的列子來看3個Timer的使用和區別

一 、System.Windows.Forms.Timer

 #region System.Windows.Forms.Timer   
    public partial class Form1 : Form
    {
        public Form1()
        {
         InitializeComponent();
        }
        int num = 0;
        private void Form_Timer_Tick(object sender, EventArgs e)
        {
            label1.Text = (++num).ToString();
            Thread.Sleep(3000);
        }
        private void button1_Click(object sender, EventArgs e)
        {
            Form_Timer.Start();
        }
        private void button2_Click(object sender, EventArgs e)
        {
            Form_Timer.Stop();
        }
    }
    #endregion

 

上面這個是一個很簡單的功能,在Form窗體上拖了一個System.Windows.Forms.Timer控制項名字為Form_Timer,在屬性窗中把Enable屬性設置為Ture,Interval是定時器的間隔時間。雙擊這個控制項就可以看到 Form_Timer_Tick方法。

在這個方法中,我們讓她不停的加一個數字並顯示在窗體上,2個按鈕提供了對計時器的控制功能。執行的時候你去點擊其他窗體在回來,你會發現我們的窗體失去響應了。

因為我們這裡使用Thread.Sleep(3000); 讓當前執行緒掛起,而UI失去相應,說明了這裡執行時候採用的是單執行緒。也就是執行定時器的執行緒就是UI執行緒。Timer 用於以用戶定義的事件間隔觸發事件。

Windows 計時器是為單執行緒環境設計的,其中,UI 執行緒用於執行處理。它要求用戶程式碼有一個可用的 UI 消息泵,而且總是在同一個執行緒中操作,或者將調用封送到另一個執行緒。

在Timer內部定義的了一個Tick事件,我們前面雙擊這個控制項時實際是增加了一行程式碼。

this.Form_Timer.Tick += new System.EventHandler(this.Form_Timer_Tick);  

然後Windows將這個定時器與調用執行緒關聯(UI執行緒)。當定時器觸發時,Windows把一個定時器消息插入到執行緒消息隊列中。調用執行緒執行一個消息泵提取消息,然後發送到回調方法中(這裡的Form_Timer_Tick方法)。

而這些都是單執行緒進行了,所以在執行回調方法時UI會假死。所以使用這個控制項不宜執行計算受限或IO受限的程式碼,因為這樣容易導致介面假死,而應該使用多執行緒調用的Timer。

另外要注意的是這個控制項時間精度不高,精度限定為 55 毫秒。

二、System.Timers.Timer

接下來就看下另一個Timer,我們用它來改寫上面的程式

#region System.Windows.Forms.Timer   
    public partial class Form1 : Form
    {
        public Form1() {
            InitializeComponent();
        }
        int num = 0;
        DateTime time1 = new DateTime();
        DateTime time2 = new DateTime();
        //定義Timer   
        System.Timers.Timer Timers_Timer = new System.Timers.Timer();
        private void button1_Click(object sender, EventArgs e)
        {
            //手動設置Timer,開始執行   
            Timers_Timer.Interval = 20;
            Timers_Timer.Enabled = true;
            Timers_Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timers_Timer_Elapsed);
            time1 = DateTime.Now;
        }
        void Timers_Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            label1.Text = Convert.ToString((++num)); //顯示到lable   
            Thread.Sleep(3000);
        }
        private void button2_Click(object sender, EventArgs e)
        {
            //停止執行   
            Timers_Timer.Enabled = false;
            time2 = DateTime.Now;
            MessageBox.Show(Convert.ToString(time2 - time1));
        }
    }
    #endregion

我們可以看到這個程式碼和前面使用Form.Timer的基本相同,不同的是我們是手動定義的對象,而不是通過拉控制項。他也有Interval ,Enabled 等屬性,作用和第一是一樣的。

不同的是他的事件名為Elapsed ,但是和上面的Tick一樣,綁定一個委託的方法。只是這裡我們是手動完成的。另外不同之處是Form.Timer我們可以用Stop和Start方法控制,而這裡是通過Enable屬性控制。

但實際上也可以用Stop和Start方法,內部也是通過他自己的Enable來控制的。

  最大的不同就是上面的程式碼在調試時會報錯,提示你”執行緒間操作無效: 從不是創建控制項「label1」的執行緒訪問它。”但如果你不調試直接運行是OK的,而且運行時你去拖動窗體會發現沒有出現假死。

從這裡我們就可以知道這裡的Timer的創建執行緒和執行執行緒不是同一個執行緒。也就是使用了多執行緒。Timer的創建執行緒是UI執行緒,而執行執行緒是TheardPool中的執行緒所以不會假死,但調試的時候會報錯,因為非控制項的創建執行緒不能操作控制項

但你可以直接運行,這裡是VS05做了手腳。解決辦法很多,用delegate.BeginInvoke()等等。這裡介紹特有的一種方法,設置Timer的SynchronizingObject屬性,Timers_Timer.SynchronizingObject = label1;

這樣調試運行時就不會報錯了,但是設置了這個屬性Timer就編程單執行緒調用了,就基本和第一個完全一樣了。

      /// 在timer中使用SynchronizingObject的原因(tangtao_xp的註解)
        /// 請參考//msdn.microsoft.com/en-us//library/system.timers.timer.synchronizingobject
        /// 1.如果SynchronizingObject為null,timer的Elapsed事件會默認由執行緒池進行處理
        /// 2.如果timer的Elapsed事件要在windows Form等UI元件中處理,就會出現UI執行緒訪問執行緒池情況
        /// 此時會引發錯誤和異常;將SynchronizingObject設置成要處理timer的Elapsed事件的UI元件,
        /// 該timer就會由該UI元件的執行緒創建,從而避免異常。

三 、System.Threading.Timer

繼續用這個對象改造程式。

#region System.Windows.Forms.Timer   
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        int num = 0;
        DateTime time1 = new DateTime();
        DateTime time2 = new DateTime();
        System.Threading.Timer Thread_Time;
        private void button1_Click(object sender, EventArgs e)
        {
            //啟動   
            Thread_Time = new System.Threading.Timer(Thread_Timer_Method, null, 0, 20);
            time1 = DateTime.Now;
        }
        void Thread_Timer_Method(object o)
        {
            label1.Text = Convert.ToString((++num));
            System.Threading.Thread.Sleep(3000);
        }
        private void button2_Click(object sender, EventArgs e)
        {
            //停止   
            Thread_Time.Dispose();
            time2 = DateTime.Now;
            MessageBox.Show(Convert.ToString(time2 - time1));
        }
    }
    #endregion

用Threading.Timer時的方法,和前面就不太相同了,所以的參數全部在構造函數中進行了設置,而且可以設置啟動時間。而且沒有提供start和stop方法來控制計時器。

而且是以一種回調方法的方式實現,而不是通過事件來實現的。他們之間還是有區別的。我們只有銷毀掉對象來停止他。

當你運行時,你會發現他和前面的Timers.Timer一樣,是多執行緒的,主要表現在不會假死,調試運行報錯。但跟讓你奇怪的是,我們的程式碼竟然無法讓她停止下來。

調用了Dispose方法沒有用。問題在那?然後有進行了測試,修改了間隔時間為100,200,500,1000,3000,4000。

這幾種情況。發現當間隔為500ms以上是基本馬上就停止了。而間隔時間相對執行時間越短,繼續執行的時間越長。這應該是在間隔時間小於執行時間時多個執行緒運行造成的。因為所有的執行緒不是同時停止的。間隔越短,執行緒越多,所以執行次數越多。

最後來看下這個對象另外一個特殊的地方。

 static void Main()
    {
        Timer t = new Timer(Test, null, 0, 1000);
        Console.ReadLine();
    }
    public static void Test(object o)
    {
        Console.WriteLine("hello world");
        GC.Collect();
    }

這段程式碼會輸出什麼結果呢?默認情況他只輸出一次,就停止了。為什麼呢?根據上面說的,當定義對象t,執行程式碼後,進行了強制垃圾回收,因為t在Main中沒有其他引用,所以被回收掉了。

但是如果我們把編譯器的」優化「項取消掉,在看看情況。程式進然一直在輸出。為什麼執行垃圾回收卻沒有被回收呢?因為這個禁用優化選項,t的聲明周期被擴展到了方法結束。所以一直執行。

因為編譯器默認是優化的,所以我們必須保證Timer對象一直被引用,而避免被垃圾回收。所以我們可以在編譯器打開優化的情況下,在Main函數最後加上t=null保證回收前被引用,但你發現,這樣是沒用的。

因為JIT編譯器優化後會吧t=null直接刪除,所以我們用t.Dispose(),就可以達到目的。在我們進行垃圾回收時,CLR發現t還有被引用,還沒執行Dispose所以不會被回收。

是以Threading.Timer有時候會出現運行一次就停止或者是銷毀了還在運行的情況,而且和編譯器優化也有關,所以使用時要注意。

最後看下MSDN的描述: 只要在使用 Timer,就必須保留對它的引用。對於任何託管對象,如果沒有對 Timer 的引用,計時器會被垃圾回收。即使 Timer 仍處在活動狀態,也會被回收。當不再需要計時器時,請使用 Dispose 方法釋放計時器持有的資源。

如果希望在計時器被釋放時接收到訊號,請使用接受 WaitHandle 的 Dispose(WaitHandle) 方法重載。計時器已被釋放後,WaitHandle 便終止。

總結:

  •   System.Threading.Timer 是一個簡單的輕量計時器,它使用回調方法並由執行緒池執行緒提供服務。不建議將其用於 Windows 窗體,因為其回調不在用戶介面執行緒上進行。System.Windows.Forms.Timer 是用於 Windows 窗體的更佳選擇。
  • 要獲取基於伺服器的計時器功能,可以考慮使用 System.Timers.Timer,它可以引發事件並具有其他功能。
  •   在《CLR Via C#》中講多執行緒時有提到這3個計時器,但作者說System.Timers.Timer是對System.Threading.Timer的報裝,不推薦使用,但是在我的WEB項目中的Application_Start中我還是使用的這個而不是Threading.Timer,因為使用Threading.Timer時只執行了一次就不在執行了。
  • 對於計時器在B/S結構中的使用就複雜一些,一般我們把計時器放在Application_OnStart中,這樣全局維護一個計時器,可以進行定期備份資料庫,定期維護用戶等操作,而且方法寫作靜態的,以免被垃圾回收
  • 而不建議在一般的aspx頁面中使用,因為伺服器端的定時器對用戶這樣意義不大,完全可以使用JS代替。而且這個頁面的每個請求都可能引入一個新的定時器,導致系統崩潰。
  • 另外,定時器是ASP.NET進程,IIS有關,所以對用重要的執行任務,還是建議寫成服務或獨立程式放在伺服器上執行好了。
  •   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控制項。它們的缺點是不支援直接的拖放,需要手工編碼。

 

ok,今天的學習就到此結束了,有什麼不對的歡迎指出~

參考部落格:

//www.cnblogs.com/SkySoot/p/3494154.html

//www.cnblogs.com/OpenCoder/archive/2010/02/23/1672043.html

Tags: