c#中多線程---使用多線程

 

單元模式和Windows Forms

單元模式線程是一個自動線程安全機制,非常貼近於COM——Microsoft的遺留下的組件對象模型。儘管.NET最大地放棄擺脫了遺留下的模型,但很多時候它也會突然出現,這是因爲有必要與舊的API 進行通信。單元模式線程與Windows Forms最相關,因爲大多Windows Forms使用或包裝了長期存在的Win32 API——連同它的單元傳統。

單元是多線程的邏輯上的“容器”,單元產生兩種容量——“單的”和“多的”。單線程單元只包含一個線程;多線程單元可以包含任何數量的線程。單線程模式更普遍並且能與兩者有互操作性。

就像包含線程一樣,單元也包含對象,當對象在一個單元內被創建後,在它的生命週期中它將一直存在在那,永遠也“居家不出”地與那些駐留線程在一起。這類似於被包含在.NET 同步環境中,除了同步環境中沒有自己的或包含線程。任何線程可以訪問在任何同步環境中的對象 ——在排它鎖的控制中。但是單元內的對象只有單元內的線程纔可以訪問。

想象一個圖書館,每本書都象徵着一個對象;借出書是不被允許的,書都在圖書館創建並直到它壽終正寢。此外,我們用一個人來象徵一個線程。

一個同步內容的圖書館允許任何人進入,同時同一時刻只允許一個人進入,在圖書館外會形成隊列。

單元模式的圖書館有常駐維護人員——對於單線程模式的圖書館有一個圖書管理員,對於多線程模式的圖書館則有一個團隊的管理員。沒人被允許除了隸屬與維護人員的人 ——資助人想要完成研究就必須給圖書管理員發信號,然後告訴管理員去做工作!給管理員發信號被稱爲調度編組——資助人通過 調度把方法依次讀出給一個隸屬管理員的人(或,某個隸屬管理員的人!)。調度編組是自動的,在Windows Forms通過信息泵被實現在庫結尾。這就是操作系統經常檢查鍵盤和鼠標的機制。如果信息到達的太快了,以致不能被處理,它們將形成消息隊列,所以它門可以以它們到達的順序被處理。

定義單元模式

.NET線程在進入單元核心Win32或舊的COM代碼前自動地給單元賦值,它被默認地指定爲多線程單元模式,除非需要一個單線程單元模式,就像下面的一樣:

Thread t = new Thread (...);
t.SetApartmentState (ApartmentState.STA);

你也可以用STAThread特性標在主線程上來讓它與單線程單元相結合:

class Program {
  [STAThread]
  static void Main() {
  ...

單元們對純.NET代碼沒有效果,換言之,即使兩個線程都有STA 的單元狀態,也可以被相同的對象同時調用相同的方法,就沒有自動的信號編組或鎖定發生了,只有在執行非託管的代碼時,這纔會發生。

System.Windows.Forms名稱空間下的類型,廣泛地調用Win32代碼,在單線程單元下工作。由於這個原因,一個Windos Forms程序應該在它的主方法上貼上 [STAThread]特性,除非在執行?Win32 UI代碼之前以下二者之一發生了:

  • 它將調度編組成一個單線程單元
  • 它將崩潰

Control.Invoke

在多線程的Windows Forms程序中,通過非創建控件的線程調用控件的的屬性和方法是非法的。所有跨進程的調用必須被明確地排列至創建控件的線程中(通常爲主線程),利用Control.InvokeControl.BeginInvoke方法。你不能依賴自動調度編組因爲它發生的太晚了,僅當執行剛好進入了非託管的代碼它才發生,而.NET已有足夠的時間來運行“錯誤的”線程代碼,那些非線程安全的代碼。

一個優秀的管理Windows Forms程序的方案是使用BackgroundWorker,這個類包裝了需要報道進度和完成度的工作線程,並自動地調用Control.Invoke方法作爲需要。

BackgroundWorker

BackgroundWorker是一個在System.ComponentModel命名空間下幫助類,它管理着工作線程。它提供了以下特性:

  • "cancel" 標記,對於給工作線程打信號讓它結束而沒有使用 Abort的情況
  • 提供報道進度,完成度和退出的標準方案
  • 實現了IComponent接口,允許它參與Visual Studio設計器
  • 在工作線程之上做異常處理
  • 更新Windows Forms控件以應答工作進度或完成度的能力

最後兩個特性是相當地有用:意味着你不再需要將try/catch語句塊放到你的工作線程中了,並且更新Windows Forms控件不需要調用 Control.Invoke了。

BackgroundWorker使用線程池工作,對於每個新任務,它循環使用避免線程們得到休息。這意味着你不能在 BackgroundWorker線程上調用 Abort了。

下面是使用BackgroundWorker最少的步驟:

  • 實例化 BackgroundWorker,爲DoWork事件增加委託。
  • 調用RunWorkerAsync方法,使用一個隨便的object參數。

這就設置好了它,任何被傳入RunWorkerAsync的參數將通過事件參數的Argument屬性,傳到DoWork事件委託的方法中,下面是例子:

class Program {
  static BackgroundWorker bw = new BackgroundWorker();
  static void Main() {
    bw.DoWork += bw_DoWork;
    bw.RunWorkerAsync ("Message to worker");     
    Console.ReadLine();
  }
 
  static void bw_DoWork (object sender, DoWorkEventArgs e) {
    // 這被工作線程調用
    Console.WriteLine (e.Argument);        // 寫"Message to worker"
    // 執行耗時的任務...
  }

BackgroundWorker也提供了RunWorkerCompleted事件,它在DoWork事件完成後觸發,處理RunWorkerCompleted事件並不是強制的,但是爲了查詢到DoWork中的異常,你通常會這麼做的。RunWorkerCompleted中的代碼可以更新Windows Forms 控件,而不用顯示的信號編組,而DoWork中就可以這麼做。

添加進程報告支持:

  • 設置WorkerReportsProgress屬性爲true
  • DoWork中使用“完成百分比”週期地調用ReportProgress方法,以及可選用戶狀態對象
  • 處理ProgressChanged事件,查詢它的事件參數的 ProgressPercentage屬性

ProgressChanged中的代碼就像RunWorkerCompleted一樣可以自由地與UI控件進行交互,這在更性進度欄尤爲有用。

添加退出報告支持:

  • 設置WorkerSupportsCancellation屬性爲true
  • DoWork中週期地檢查CancellationPending屬性:如果爲true,就設置事件參數的Cancel屬性爲true,然後返回。(工作線程可能會設置Cancel爲true,並且不通過CancellationPending進行提示——如果判定工作太過困難並且它不能繼續運行)
  • 調用CancelAsync來請求退出

下面的例子實現了上面描述的特性:

using System;
using System.Threading;
using System.ComponentModel;
 
class Program {
  static BackgroundWorker bw;
  static void Main() {
    bw = new BackgroundWorker();
    bw.WorkerReportsProgress = true;
    bw.WorkerSupportsCancellation = true;
    bw.DoWork += bw_DoWork;
    bw.ProgressChanged += bw_ProgressChanged;
    bw.RunWorkerCompleted += bw_RunWorkerCompleted;
 
    bw.RunWorkerAsync ("Hello to worker");
    
    Console.WriteLine ("Press Enter in the next 5 seconds to cancel");
    Console.ReadLine();
    if (bw.IsBusy) bw.CancelAsync();
    Console.ReadLine();
  }
 
  static void bw_DoWork (object sender, DoWorkEventArgs e) {
    for (int i = 0; i <= 100; i += 20) {
      if (bw.CancellationPending) {
        e.Cancel = true;
        return;
      }
      bw.ReportProgress (i);
      Thread.Sleep (1000);
    }
    e.Result = 123;    // 傳遞給 RunWorkerCopmleted
  }
 
  static void bw_RunWorkerCompleted (object sender,
  RunWorkerCompletedEventArgs e) {
    if (e.Cancelled)
      Console.WriteLine ("You cancelled!");
    else if (e.Error != null)
      Console.WriteLine ("Worker exception: " + e.Error.ToString());
    else
      Console.WriteLine ("Complete - " + e.Result);      // 從 DoWork
  }
 
  static void bw_ProgressChanged (object sender,
  ProgressChangedEventArgs e) {
    Console.WriteLine ("Reached " + e.ProgressPercentage + "%");
  }
}

Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%
Reached 60%
Reached 80%
Reached 100%
Complete – 123

Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%

You cancelled!

BackgroundWorker的子類

BackgroundWorker不是密封類,它提供OnDoWork爲虛方法,暗示着另一個模式可以它。當寫一個可能耗時的方法,你可以或最好寫個返回BackgroundWorker子類的等方法,預配置完成異步的工作。使用者只要處理RunWorkerCompleted事件和ProgressChanged事件。比如,設想我們寫一個耗時的方法叫做GetFinancialTotals

public class Client {
  Dictionary <string,int> GetFinancialTotals (int foo, int bar) { ... }
  ...
}

我們可以如此來實現:

public class Client {
  public FinancialWorker GetFinancialTotalsBackground (int foo, int bar) {
    return new FinancialWorker (foo, bar);
  }
}
 
public class FinancialWorker : BackgroundWorker {
  public Dictionary <string,int> Result;   // 我們增加類型字段
  public volatile int Foo, Bar;            // 我們甚至可以暴露它們
                                           // 通過鎖的屬性!
  public FinancialWorker() {
    WorkerReportsProgress = true;
    WorkerSupportsCancellation = true;
  }
 
  public FinancialWorker (int foo, int bar) : this() {
    this.Foo = foo; this.Bar = bar;
  }
 
  protected override void OnDoWork (DoWorkEventArgs e) {
    ReportProgress (0, "Working hard on this report...");
    Initialize financial report data
 
    while (!finished report ) {
      if (CancellationPending) {
        e.Cancel = true;
        return;
      }
      Perform another calculation step
      ReportProgress (percentCompleteCalc, "Getting there...");
    }      
    ReportProgress (100, "Done!");
    e.Result = Result = completed report data;
  }
}

無論誰調用GetFinancialTotalsBackground都會得到一個FinancialWorker——一個用真實地可用地包裝了管理後臺操作。它可以報告進度,被取消,與Windows Forms交互而不用使用Control.Invoke。它也有異常句柄,並且使用了標準的協議(與使用BackgroundWorker沒任何區別!)

這種BackgroundWorker的用法有效地迴避了舊有的“基於事件的異步模式”。

ReaderWriterLock類

通常來講,一個類型的實例對於並行的讀操作是線程安全的,但是並行地根性操作則不是(並行地讀和更新也不是)。這對於資源也是一樣的,比如一個文件。當保護類型的實例安全時,使用一個簡單的排它鎖即解決問題,但是當有很多的讀操作而偶然的更新操作這就很不合理的限制了併發。一個例子就是這在一個業務程序服務器中,爲了快速查找把數據緩存到靜態字段中。在這個方案中,ReaderWriterLock類被設計成提供最大容量的鎖定。

ReaderWriterLock爲讀和寫的鎖提供了不同的方法——AcquireReaderLockAcquireWriterLock。兩個方法都需要一個超時參數,並且在超時發生後拋出ApplicationException異常(不同於大多數線程類的返回false等效的方法)。超時發生相當容易在資源爭用嚴重的時候。

調用 ReleaseReaderLockReleaseWriterLock釋放鎖。這些方法支持嵌套鎖,ReleaseLock方法也支持一次清除所有嵌套級別的鎖。(你可以隨後調用RestoreLock類重新鎖定相同的級別,它在ReleaseLock之前執行——如此來模仿Monitor.Wait鎖定切換行爲)。

你可以調用AcquireReaderLock開始一個read-lock ,然後通過UpgradeToWriterLock把它升級爲write-lock。這個方法返回一個可能被用於調用DowngradeFromWriterLock的信息。這個方式允許讀程序臨時地請求寫訪問同時不必必須在降級之後重新排隊列。

在接下來的這個例子中,4個線程被啓動:一個不停地往列表中增加項目;另一個不停地從列表中移除項目;其它兩個不停地報告列表中項目的個數。前兩者獲得寫的鎖,後兩者獲得讀的鎖。每個鎖的超時參數爲10秒。(異常處理一般要使用來捕捉ApplicationException,這個例子中出於方便而省略了)

class Program {
  static ReaderWriterLock rw = new ReaderWriterLock ();
  static List <int> items = new List <int> ();
  static Random rand = new Random ();
 
  static void Main (string[] args) {
    new Thread (delegate() { while (true) AppendItem(); } ).Start();
    new Thread (delegate() { while (true) RemoveItem(); } ).Start();
    new Thread (delegate() { while (true) WriteTotal(); } ).Start();
    new Thread (delegate() { while (true) WriteTotal(); } ).Start();
  }
 
  static int GetRandNum (int max) { lock (rand) return rand.Next (max); }
 
  static void WriteTotal() {
    rw.AcquireReaderLock (10000);
    int tot = 0; foreach (int i in items) tot += i;
    Console.WriteLine (tot);
    rw.ReleaseReaderLock();
  }
 
 static void AppendItem () {
    rw.AcquireWriterLock (10000);
    items.Add (GetRandNum (1000));
    Thread.SpinWait (400);
    rw.ReleaseWriterLock();
  }
 
  static void RemoveItem () {
    rw.AcquireWriterLock (10000);
    if (items.Count > 0)
      items.RemoveAt (GetRandNum (items.Count));
    rw.ReleaseWriterLock();
  }
}

List中加項目要比移除快一些,這個例子在AppendItem中包含了SpinWait來保持項目總數平衡。

線程池

如果你的程序有很多線程,導致花費了大多時間在等待句柄的阻止上,你可以通過 線程池來削減負擔。線程池通過合併很多等待句柄在很少的線程上來節省時間。

使用線程池,你需要註冊一個連同將被執行的委託的Wait Handle,在Wait Handle發信號時。這個工作通過調用ThreadPool.RegisterWaitForSingleObject來完成,如下:

class Test {
  static ManualResetEvent starter = new ManualResetEvent (false);
 
  public static void Main() {
    ThreadPool.RegisterWaitForSingleObject (starter, Go, "hello", -1, true);
    Thread.Sleep (5000);
    Console.WriteLine ("Signaling worker...");
    starter.Set();
    Console.ReadLine();
  }
 
  public static void Go (object data, bool timedOut) {
    Console.WriteLine ("Started " + data);
    // 完成任務...
  }
}

(5 second delay)
Signaling worker...
Started hello

除了等待句柄和委託之外,RegisterWaitForSingleObject也接收一個“黑盒”對象,它被傳遞到你的委託方法中( 就像用ParameterizedThreadStart一樣),擁有一個毫秒級的超時參數(-1意味着沒有超時)和布爾標誌來指明請求是一次性的還是循環的。

所有進入線程池的線程都是後臺的線程,這意味着它們在程序的前臺線程終止後將自動的被終止。但你如果想等待進入線程池的線程都完成它們的重要工作在退出程序之前,在它們上調用Join是不行的,因爲進入線程池的線程從來不會結束!意思是說,它們被改爲循環,直到父進程終止後才結束。所以爲知道運行在線程池中的線程是否完成,你必須發信號——比如用另一個Wait Handle。

在線程池中的線程上調用Abort 是一個壞主意,線程需要在程序域的生命週期中循環。

你也可以用QueueUserWorkItem方法而不用等待句柄來使用線程池,它定義了一個立即執行的委託。你不必在多個任務中取得節省共享線程,但有一個慣例:線程池保持一個線程總數的封頂(默認爲25),在任務數達到這個頂值後將自動排隊。這就像程序範圍的有25個消費者的生產者/消費者隊列。在下面的例子中,100個任務入列到線程池中,而一次只執行 25個,主線程使用Wait 和 Pulse來等待所有的任務完成:

class Test {
  static object workerLocker = new object ();
  static int runningWorkers = 100;
 
  public static void Main() {
    for (int i = 0; i < runningWorkers; i++) {
      ThreadPool.QueueUserWorkItem (Go, i);
    }
    Console.WriteLine ("Waiting for threads to complete...");
    lock (workerLocker) {
      while (runningWorkers > 0) Monitor.Wait (workerLocker);
    }
    Console.WriteLine ("Complete!");
    Console.ReadLine();
  }
 
  public static void Go (object instance) {
    Console.WriteLine ("Started: " + instance);
    Thread.Sleep (1000);
    Console.WriteLine ("Ended: " + instance);
    lock (workerLocker) {
      runningWorkers--; Monitor.Pulse (workerLocker);
    }
  }
}

爲了傳遞多餘一個對象給目標方法,你可以定義個擁有所有需要屬性自定義對象,或者調用一個匿名方法。比如如果Go方法接收兩個整型參數,會像下面這樣:

ThreadPool.QueueUserWorkItem (delegate (object notUsed) { Go (23,34); });

另一個進入線程池的方式是通過異步委託

異步委託

在第一部分我們描述如何使用 ParameterizedThreadStart把數據傳入線程中。有時候你需要通過另一種方式,來從線程中得到它完成後的返回值。異步委託提供了一個便利的機制,允許許多參數在兩個方向上傳遞。此外,未處理的異常在異步委託中在原始線程上被重新拋出,因此在工作線程上不需要明確的處理了。異步委託也提供了計入線程池的另一種方式。

對此你必須付出的代價是要跟從異步模型。爲了看看這意味着什麼,我們首先討論更常見的同步模型。我們假設我們想比較兩個web頁面,我們按順序取得它們,然後像下面這樣比較它們的輸出:

static void ComparePages() {
  WebClient wc = new WebClient ();
  string s1 = wc.DownloadString ("http://www.oreilly.com");
  string s2 = wc.DownloadString ("http://oreilly.com");
  Console.WriteLine (s1 == s2 ? "Same" : "Different");
}

如果兩個頁面同時下載當然會更快了。問題在於當頁面正在下載時DownloadString阻止了繼續調用方法。如果我們能調用 DownloadString在一個非阻止的異步方式中會變的更好,換言之:

  1. 我們告訴 DownloadString 開始執行
  2. 在它執行時我們執行其它任務,比如說下載另一個頁面
  3. 我們詢問DownloadString的所有結果

WebClient類實際上提供一個被稱爲DownloadStringAsync的內建方法,它提供了就像異步函數的功能。而眼下,我們忽略這個問題,集中精力在任何方法都可以被異步調用的機制上。

第三步使異步委託變的有用。調用者彙集了工作線程得到結果和允許任何異常被重新拋出。沒有這步,我們只有普通多線程。雖然也可能不用匯集方式使用異步委託,你可以用ThreadPool.QueueWorkerItemBackgroundWorker

下面我們用異步委託來下載兩個web頁面,同時實現一個計算:

delegate string DownloadString (string uri);

 

 
static void ComparePages() {
 
  // 實例化委託DownloadString:
  DownloadString download1 = new WebClient().DownloadString;
  DownloadString download2 = new WebClient().DownloadString;
  
  // 開始下載:
  IAsyncResult cookie1 = download1.BeginInvoke (uri1, null, null);
  IAsyncResult cookie2 = download2.BeginInvoke (uri2, null, null);
  
  // 執行一些隨機的計算:
  double seed = 1.23;
  for (int i = 0; i < 1000000; i++) seed = Math.Sqrt (seed + 1000);
  
  // 從下載獲取結果,如果必要就等待完成
  // 任何異常在這拋出:
  string s1 = download1.EndInvoke (cookie1);
  string s2 = download2.EndInvoke (cookie2);
  
  Console.WriteLine (s1 == s2 ? "Same" : "Different");
}

我們以聲明和實例化我們想要異步運行的方法開始。在這個例子中,我們需要兩個委託,每個引用不同的WebClient的對象(WebClient 不允許並行的訪問,如果它允許,我們就只需一個委託了)。

我們然後調用BeginInvoke,這開始執行並立刻返回控制器給調用者。依照我們的委託,我們必須傳遞一個字符串給 BeginInvoke (編譯器由生產BeginInvokeEndInvoke在委託類型強迫實現這個).

BeginInvoke 還需要兩個參數:一個可選callback和數據對象;它們通常不需要而被設置爲null, BeginInvoke返回一個 IASynchResult對象,它擔當着調用 EndInvoke所用的數據。IASynchResult 同時有一個IsCompleted屬性來檢查進度。

之後我們在委託上調用EndInvoke ,得到需要的結果。如果有必要,EndInvoke會等待,直到方法完成,然後返回方法返回的值作爲委託指定的(這裏是字符串)。 EndInvoke一個好的特性是DownloadString有任何的引用或輸出參數,它們會在 EndInvoke結構賦值,允許通過調用者多個值被返回。

在異步方法的執行中的任何點發生了未處理的異常,它會重新在調用線程在EndInvoke中拋出。這提供了精簡的方式來管理返回給調用者的異常。

如果你異步調用的方法沒有返回值,你也(學理上的)應該調用EndInvoke,在部分意義上在開放了誤判;MSDN上辯論着這個話題。如果你選擇不調用EndInvoke,你需要考慮在工作方法中的異常。

異步方法

.NET Framework 中的一些類型提供了某些它們方法的異步版本,它們使用"Begin" 和 "End"開頭。它們被稱之爲異步方法,它們有與異步委託類似的特性,但存在着一些待解決的困難的問題:允許比你所擁有的線程還多的併發活動率。比如一個web或TCP Socket服務器,如果用NetworkStream.BeginReadNetworkStream.BeginWrite來寫的話,可能在僅僅一把線程池線程中處理數百個併發的請求。

除非你寫了一個專門的高併發程序,儘管如此,你還是應該如下理由儘量避免異步方法:

  • 不像異步委託,異步方法實際上可能沒有與調用者同時執行
  • 異步方法的好處被侵腐或消失了,如果你未能小心翼翼地遵從它的模式
  • 當你恰當地遵從了它的模式,事情立刻變的複雜了

如果你只是像簡單地獲得並行執行的結果,你最好遠離調用異步版本的方法(比如NetworkStream.Read)而通過異步委託。另一個選項是使用 ThreadPool.QueueUserWorkItemBackgroundWorker,又或者只是簡單地創建新的線程。

異步事件

另一種模式存在,就是爲什麼類型可以提供異步版本的方法。這就是所謂的“基於事件的異步模式”,是一個傑出的方法以"Async"結束,相應的事件以"Completed"結束。WebClient使用這個模式在它的DownloadStringAsync 方法中。爲了使用它,你要首先處理"Completed" 事件(例如:DownloadStringCompleted),然後調用"Async"方法(例如:DownloadStringAsync)。當方法完成後,它調用你事件句柄。不幸的是,WebClient的實現是有缺陷的:像DownloadStringAsync 這樣的方法對於下載的一部分時間阻止了調用者的線程。

基於事件的模式也提供了報道進度和取消操作,被有好地設計成可對Windows程序可更新forms和控件。如果在某個類型中你需要這些特性,而它卻不支持(或支持的不好)基於事件的模式,你沒必要去自己實現它(你也根本不想去做!)。儘管如此,所有的這些通過 BackgroundWorker這個幫助類便可輕鬆完成。

計時器

週期性的執行某個方法最簡單的方法就是使用一個計時器,比如System.Threading 命名空間下Timer類。線程計時器利用了線程池,允許多個計時器被創建而沒有額外的線程開銷。 Timer 算是相當簡易的類,它有一個構造器和兩個方法(這對於一個低限度要求者或是書的作者來說是最高興不過的了)。

public sealed class Timer : MarshalByRefObject, IDisposable
{
  public Timer (TimerCallback tick, object state, 1st, subsequent);
  public bool Change (1st, subsequent);   // 改變時間間隔
  public void Dispose();                // 幹掉timer
}
1st = 第一次觸發的時間,使用毫秒或TimeSpan
subsequent = 後來的間隔,使用毫秒或TimeSpan
 (爲了一次性的調用使用 Timeout.Infinite)

接下來這個例子,計時器5秒鐘之後調用了Tick 的方法,它寫"tick...",然後每秒寫一個,直到用戶敲 Enter

using System;
using System.Threading;
 
class Program {
  static void Main() {
    Timer tmr = new Timer (Tick, "tick...", 5000, 1000);
    Console.ReadLine();
    tmr.Dispose();         // 結束timer
  }
 
  static void Tick (object data) {
    // 運行在線程池裏
    Console.WriteLine (data);          // 寫 "tick..."
  }
}

.NET framework在System.Timers命名空間下提供了另一個計時器類。它完全包裝自System.Threading.Timer,在使用相同的線程池時提供了額外的便利——相同的底層引擎。下面是增加的特性的摘要:

  • 實現了Component,允許它被放置到Visual Studio設計器中
  • Interval屬性代替了Change方法
  • Elapsed 事件代替了callback委託
  • Enabled屬性開始或暫停計時器
  • 提夠StartStop方法,萬一對Enabled感到迷惑
  • AutoReset標誌來指示是否循環(默認爲true)

例子:

using System;
using System.Timers;   // Timers 命名空間代替Threading
 
class SystemTimer {
  static void Main() {
    Timer tmr = new Timer();       // 不需要任何參數
    tmr.Interval = 500;
    tmr.Elapsed += tmr_Elapsed;    // 使用event代替delegate
    tmr.Start();                   // 開始timer
    Console.ReadLine();
    tmr.Stop();                    // 暫停timer
    Console.ReadLine();
    tmr.Start();                   // 恢復 timer
    Console.ReadLine();
    tmr.Dispose();                 // 永久的停止timer
  }
 
  static void tmr_Elapsed (object sender, EventArgs e) {
    Console.WriteLine ("Tick");
  }
}

.NET framework 還提供了第三個計時器——在System.Windows.Forms 命名空間下。雖然類似於System.Timers.Timer 的接口,但功能特性上有根本的不同。一個Windows Forms 計時器不能使用線程池,代替爲總是在最初創建它的線程上觸發 "Tick"事件。假定這是主線程——負責實例化所有Windows Forms程序中的forms和控件,計時器的事件句柄是能高於forms和控件結合的而不違反線程安全——或者強加單元線程模式Control.Invoke是不需要的。

Windows Forms計時器可能迅速地執行來更新用戶接口。迅速地執行是重要的,因爲Tick事件被主線程調用,如果它有停頓,將使用戶接口變的沒有響應。

局部存儲

每個線程與其它線程數據存儲是隔離的,這對於“不相干的區域”的存儲是有益的,它支持執行路徑的基礎結構,如通信,事務和安全令牌。通過這些環繞在方法參數的數據將極端的粗劣並與你的本身的方法隔離開;在靜態字段裏存儲信息意味在所有線程中共享它們。

Thread.GetData從一個線程的隔離數據中讀,Thread.SetData 寫。兩個方法需要一個LocalDataStoreSlot對象來識別內存槽——這包裝自一個內存槽的名稱的字符串,這個名稱你可以跨所有的線程使用,它們將得到不各自的值,看這個例子:

class ... {
  // 相同的LocalDataStoreSlot 對象可以用於跨所有線程
  LocalDataStoreSlot secSlot = Thread.GetNamedDataSlot ("securityLevel");
 
  // 這個屬性每個線程有不同的值
  int SecurityLevel {
    get {
      object data = Thread.GetData (secSlot);
      return data == null ? 0 : (int) data;    // null == 未初始化
    }
    set {
      Thread.SetData (secSlot, value);
    }
  }
  ...

Thread.FreeNamedDataSlot將釋放給定的數據槽,它跨所有的線程——但只有一次,當所有相同名字LocalDataStoreSlot對象作爲垃圾被回收時退出作用域時發生。這確保了線程不得到數據槽從它們的腳底下撤出——也保持了引用適當的使用之中的LocalDataStoreSlot對象。

發佈了26 篇原創文章 · 獲贊 1 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章