C#中的執行緒(三)多執行緒

weixin_34262482發表於2012-09-03

Keywords:C# 執行緒
Source:http://www.albahari.com/threading/
Author: Joe Albahari
Translator: Swanky Wu
Published: http://www.cnblogs.com/txw1958/
Download:http://www.albahari.info/threading/threading.pdf 

 

第三部分:使用多執行緒

 

單元模式和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.Invoke  Control.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.QueueWorkerItem  BackgroundWorker

下面我們用非同步委託來下載兩個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 (編譯器由生產BeginInvoke  EndInvoke在委託型別強迫實現這個).

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.BeginRead  NetworkStream.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屬性開始或暫停計時器
  • 提夠Start  Stop方法,萬一對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物件。

相關文章