談談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: