CSharp中的多線程——使用多線程


單元模式和Windows Forms

單元是多線程的邏輯上的“容器”,單元產生兩種容量——“單的”和“多的”。單線 程單元只包含一個線程;多線程單元可以包含任 何數量的線程。單線程模式更普遍 並且能與兩者有互操作性。 就像包含線程一樣,單元也包含對象,當對象在一個單元內被創建后,在它的生 命周期中它將一直存在在那,永遠也“居家不 出”地與那些駐留線程在一起。在排它鎖的控制中,任何線程可以訪問在任何同步環境中的對象。但是單元內的對象只有單元內的線程才可以訪問。

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

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

單元模式的圖書館有常駐維護人員——對於單線程模式的圖書館有一個圖書管理員, 對於多線程模式的圖書館則有一個團隊的 管理員。

資助人想要完成研究就必須給圖書管理員發信號,然后告訴管理員去做工 作,除了隸屬與維護人員的人沒人被允許!給管 理員發信號被稱為調度編組——資助人通過 調度把方法依次讀出給一個隸屬管理員的人(或,某個隸屬管理員的 人!)。在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已有足夠的時間來運行“錯誤的”線程代碼, 那些非線程安全的代碼。

一、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 + "%");

}

}

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...");

// 初始化計算數據
//bool finishedReport; 
while (!finishedReport{

if (CancellationPending) {

e.Cancel = true;

return;

}

//int percentCompleteCalc 編寫其它的計算步驟

ReportProgress (percentCompleteCalc , "Getting there...");

}

ReportProgress (100, "Done!");

e.Result = Result = "完成時返回的數據";

}

}

二、ReaderWriterLock類

通常來講,一個類型的實例對於並行的讀操作是線程安全的,但是並行地更新操作則不是(並行地讀和更新也不是)。 這對於 資源也是一樣的,比如一個文件。當保護類型的實例安全時,使用一個簡單的排它鎖即解決問題,但是當有很多的讀操作 而偶 然的更新操作這就很不合理的限制了並發。

ReaderWriterLock為讀和寫的鎖提供了不同的方法——AcquireReaderLock和AcquireWriterLock。兩個方法都需 要一個超時參數,並且在超時發生后拋出ApplicationException異常。在資源爭用嚴重的時候,很容易超時。

調用 ReleaseReaderLock或ReleaseWriterLock釋放鎖。 這些方法支持嵌套鎖,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妙中的延時后輸入:Signaling worker...Started hello

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

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

你也可以用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); } }
}

為了傳遞多余一個對象給目標方法,你可以定義個擁有所有需要屬性自定義對象,或者調用一個匿名方法。

四、異步委托

異步委托提供了一個便利的機制,允許許多參數在兩個方向上傳遞 。此外,在異步委托中未處理的異常在原始線程上被重新拋出,因此在工作線程上不需要明確的處理了。

相對於異步模型,我們先來看下更常見的同步模型。假設我們想比較 兩 個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會變的更好,換言之:

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

第三步使異步委托變的有用。調用者匯集了工作線程得到結果和允許任何異常被重新拋出。沒有這步,我們只有普通多線程。

下面我們用異步委托來下載兩個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一個好的特性是作為委托指定的方法有任何的引用或輸出參數,它們 會在 EndInvoke結構賦值,允許通過調用者多個值被返回。

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

如果你異步調用的方法沒有返回值,理論上也應該調用EndInvoke。如果不調用EndInvoke,需要考慮處理在工作方法中的異常。

異步方法

除非寫了一個專門的高並發程序,盡管如此,還是應該盡量避免異步方法。如果只是像簡單地獲得並行執行的結果,你最好遠離調用異步版本的方法而通過異步委托。

異步事件

基於事件的異步模式是以"Async"結束,相應的事件以"Completed"結束。

WebClient使用這個模式在它的DownloadStringAsync 方法中。 為了 使用它,你要首先處理"Completed" 事件(例如:DownloadStringCompleted),然后調用"Async"方法(例如:DownloadStringAsync)。 當方法完成后,它調用你事件句柄。不幸的是,WebClient的實現是 有缺陷的: 像DownloadStringAsync 這樣的方法對於下載的一部分時間阻止了調用者的線程。基於事件的模式也提供了報道進度和取消操作,被友好地設計成可對Windows程序可更新forms和控件。

計時器

周期性的執行某個方法最簡單的方法就是使用一個計時器,比如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"事件。

五、局部存儲

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

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將一次釋放給定的數據槽,它跨所有的線程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM