C#多線程實踐——建立和開始使用

來源:互聯網
上載者:User

標籤:

線程用Thread類來建立, 通過ThreadStart委託來指明方法從哪裡開始運行。ThreadStart的聲明如下: 

public delegate void ThreadStart();

  調用Start方法後,線程開始運行,直到它所調用的方法返回後結束。

class ThreadTest {  static void Main() {    Thread t = new Thread (new ThreadStart (Go));    t.Start();       Go();          }  static void Go() { Console.WriteLine ("hello!"); }

  一個線程可以通過C#的委託簡短的文法更便利地建立出來:

static void Main() {  Thread t = new Thread (Go);    // 不需要顯式聲明使用 ThreadStart  t.Start();  ...}static void Go() { ... }在這種情況,ThreadStart被編譯器自動推斷出來:

  另一個快捷的方式是使用匿名方法來啟動線程

static void Main() {  Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); });  t.Start();}

  線程有一個IsAlive屬性,在調用Start()之後直到線程結束之前一直為true。一個線程一旦結束便不能重新開始了。

將資料傳入ThreadStart中

    假如想更好地區分開每個線程的輸出結果,如讓其中一個線程輸出大寫字母。可以考慮傳入一個狀態字到Go中來完成整個任務,此時就不能使用ThreadStart委託,因為它不接受參數。不過NET framework定義了另一個版本的委託叫ParameterizedThreadStart, 它可以接收一個單獨的object型別參數,委託聲明如下:

public delegate void ParameterizedThreadStart (object obj);

  樣本如下:

class ThreadDemo {  static void Main()  {    Thread t = new Thread (Go); // 編譯器自動推斷    t.Start (true);             // == Go (true)     Go (false);  }  static void Go (object upperCase)  {    bool upper = (bool) upperCase;    Console.WriteLine (upper ? "HELLO!" : "hello!");  }

  在整個例子中,編譯器自動推斷出ParameterizedThreadStart委託,因為Go方法接收一個單獨的object參數,就像這樣寫:

Thread t = new Thread (new ParameterizedThreadStart (Go));t.Start (true);

  ParameterizedThreadStart的特性是在使用之前我們必需對我們想要的類型(這裡是bool)進行裝箱操作,並且它只能接收一個參數。

   一個替代方案是使用一個匿名方法調用一個普通的方法如下:

static void Main() {  Thread t = new Thread (delegate() { WriteText ("Hello"); });  t.Start();}static void WriteText (string text) { Console.WriteLine (text); }

  優點是目標方法(這裡是WriteText)可以接收任意數量的參數,並且沒有裝箱操作。不過這需要將一個外部變數放入到匿名方法中,向下面的一樣:

static void Main() {  string text = "Before";  Thread t = new Thread (delegate() { WriteText (text); });  text = "After";  t.Start();}static void WriteText (string text) { Console.WriteLine (text); }

  匿名方法出現了一種怪異的現象:當外部變數被後面的代碼修改了值的時候,線程可能會通過外部變數進行無意的互動。換個角度看,有意的互動(通常通過欄位)也可以採用這種方式!一旦線程開始運行了,外部變數最好被處理成唯讀——除非有人願意使用適當的鎖。

  另一種較常見的方式是將對象執行個體的方法而不是靜態方法傳入到線程中,對象執行個體的屬性可以告訴線程要做什麼,如下重寫了上節的例子:

class ThreadDemo{  bool upper;    static void Main()  {    ThreadDemo instance1 = new ThreadDemo();    instance1.upper = true;    Thread t = new Thread (instance1.Go);    t.Start();    ThreadDemo instance2 = new ThreadDemo();    instance2.Go();        // 主線程——運行 upper=false  }    void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }

命名線程

     線程可以通過它的Name屬性進行命名,這非常有利於調試:可以用Console.WriteLine列印出線程的名字,Microsoft Visual Studio可以將線程的名字顯示在調試工具列的位置上。線程的名字可以在被任何時間設定——但只能設定一次,重新命名會引發異常。

     程式的主線程也可以被命名,下面例子裡主線程通過CurrentThread命名:

class ThreadNaming {  static void Main()   {    Thread.CurrentThread.Name = "main";    Thread worker = new Thread (Go);    worker.Name = "worker";    worker.Start();    Go();  }  static void Go()  {    Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);  }}

前台和後台線程

     線程預設為前台線程,這意味著任何前台線程在運行都會保持程式存活。C#也支援後台線程,當所有前台線程結束後,它們不維持程式的存活。

     改變線程從前台到後台不會以任何方式改變它在CPU協調程式中的優先順序和狀態。

     線程的IsBackground屬性控制它的前後台狀態,如下執行個體:

class PriorityTest {  static void Main (string[] args)   {    Thread worker = new Thread (delegate() { Console.ReadLine(); });    if (args.Length > 0) worker.IsBackground = true;    worker.Start();  }}

  如果程式被調用的時候沒有任何參數,背景工作執行緒為前台線程,並且將等待ReadLine語句來等待使用者的觸發斷行符號,這期間,主線程退出,但是程式保持運行,因為一個前台線程仍然活著。 另一方面如果有參數傳入Main(),背景工作執行緒被賦值為後台線程,當主線程結束程式立刻退出,終止了ReadLine。後台線程這種終止方式,使任何最後操作都被規避了,這是不太合適的。好的方式是明確等待任何後台背景工作執行緒完成後再結束程式,可能用一個timeout(大多用Thread.Join)。如果因為某種原因某個背景工作執行緒無法完成,可以試圖終止它,如果失敗了,再拋棄線程,允許它與進程一起消亡。(記錄是一個難題,但在這個情境下是有意義的)

      擁有一個後台背景工作執行緒是有益的,最直接的理由是結束程式時它可能有最後的發言權,與不會消亡的前台線程一起保證程式的正常退出。拋棄一個前台背景工作執行緒風險更大,尤其對Windows Forms程式,因為程式直到主線程結束時才退出(至少對使用者來說),但是它的進程仍然運行著。它將從應用程式欄消失不見,但卻可以在在Windows工作管理員進程欄找到它。除非手動找到並結束它,否則將繼續消耗資源,並可能阻止一個新的執行個體的重新開始運行或影響它的特性。

      對於程式失敗退出的普遍原因就是存在“被忘記”的前台線程。

線程優先順序

     線程的Priority 屬性確定了線程相對於其它同一進程的活動的線程擁有多少執行時間,以下是層級:

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

  只有多個線程同時為活動時,優先順序才有作用。

    設定一個線程的優先順序為高一些,並不意味著它能執行即時的工作,因為它受限於程式的進程層級。要執行即時的工作,必須提升在System.Diagnostics 命名空間下Process的層級,像下面這樣:

Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

    ProcessPriorityClass.High 其實是一個時間片中的最高優先順序別:Realtime。設定進程層級到Realtime通知作業系統:你不想讓你的進程被搶佔了。如果你的程式進入一個偶然的死迴圈,可以預期,作業系統被鎖住了,除了關機沒有什麼可以拯救你了!基於此,High大體上被認為最高的有用進程層級。

     如果一個即時的程式有一個使用者介面,提升進程的層級是不太好的,因為當使用者介面UI過於複雜的時候,介面的更新耗費過多的CPU時間,拖慢了整台電腦。 降低主線程的層級、提升進程的層級、確保即時線程不進行介面重新整理,但這樣並不能避免電腦越來越慢,因為作業系統仍會撥出過多的CPU給整個進程。最理想的方案是使即時工作和使用者介面在不同的進程(擁有不同的優先順序)運行,通過Remoting或共用記憶體方式進行通訊,共用記憶體需要Win32 API中的 P/Invoking。(可以搜尋看看CreateFileMapping  MapViewOfFile)

異常處理

  任何線程建立範圍內try/catch/finally塊,當線程開始執行便不再與其有任何關係。考慮下面的程式:

public static void Main() { try  {   new Thread (Go).Start(); } catch (Exception ex) {   // 不會在這得到異常   Console.WriteLine ("Exception!"); }  static void Go() { throw null; }}

  這裡try/catch語句一點用也沒有,新建立的線程將引發NullReferenceException異常。當你考慮到每個線程有獨立的執行路徑的時候,便知道這行為是有道理的。補救方法是線上程處理的方法內加入他們自己的異常處理。

public static void Main() {   new Thread (Go).Start();}  static void Go() {  try {    ...    throw null;      // 這個異常在下面會被捕捉到    ...  }  catch (Exception ex) {    記錄異常日誌,並且或通知另一個線程    我們發生錯誤    ...  }

  從.NET 2.0開始,任何線程內的未處理的異常都將導致整個程式關閉,這意味著忽略異常不再是一個選項了。因此為了避免由未處理異常引起的程式崩潰,try/catch塊需要出現在每個線程進入的方法內,至少要在產品程式中應該如此。對於經常使用“全域”異常處理的Windows Forms程式員來說,這可能有點麻煩,像下面這樣:

using System;using System.Threading;using System.Windows.Forms;  static class Program {  static void Main() {    Application.ThreadException += HandleError;    Application.Run (new MainForm());  }    static void HandleError (object sender, ThreadExceptionEventArgs e) {    記錄異常或者退出程式或者繼續運行...  }}

  Application.ThreadException事件在異常被拋出時觸發,以一個Windows資訊(比如:鍵盤,滑鼠活著 "paint" 等資訊)的方式,簡言之,一個Windows Forms程式的幾乎所有代碼。雖然這看起來很完美,它使人產生一種虛假的安全感——所有的異常都被中央異常處理捕捉到了。由背景工作執行緒拋出的異常便是一個沒有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的代碼,包括構造器的形式,在Windows資訊開始前先執行) 

 .NET framework為全域異常處理提供了一個更低層級的事件:AppDomain.UnhandledException,這個事件在任何類型的程式(有或沒有使用者介面)的任何線程有任何未處理的異常觸發。儘管它提供了好的不得已的異常處理解決機制,但是這不意味著這能保證程式不崩潰,也不意味著能取消.NET異常對話方塊。

 在產品程式中,明確地使用異常處理在所有線程進入的方法中是必要的,可以使用封裝類和協助類來分解工作來完成任務,比如使用BackgroundWorker類。

C#多線程實踐——建立和開始使用

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.