託管線程處理的最佳作法

託管線程處理的最佳作法

MSDN程序員

多線程編程須要在編程時倍加註意。對於多數任務,經過將執行請求以線程池線程的方式排隊,能夠下降複雜性。本主題將探討更復雜的情形,好比協調多個線程的工做或處理形成阻止的線程。算法

死鎖和爭用條件

多線程編程解決了吞吐量和響應性問題,但引入此功能會帶來新的問題:死鎖和爭用條件。編程

死鎖

當兩個線程中的每個線程都在試圖鎖定另一個線程已鎖定的資源時,就會發生死鎖。其中任何一個線程都不能繼續執行。設計模式

託管線程處理類的許多方法都提供了超時設定,可幫您檢測到死鎖。例如,下面的代碼試圖獲取對當前實例的鎖定。若是在 300 毫秒內未能鎖定,Monitor.TryEnter 將返回 false安全

if (Monitor.TryEnter(this, 300)) {
    try {
        // Place code protected by the Monitor here.
    }
    finally {
        Monitor.Exit(this);
    }
}
else {
    // Code to execute if the attempt times out.
}

爭用條件

爭用條件是當程序的結果取決於兩個或更多個線程中的哪個先到達某一特定代碼塊時出現的一種 Bug。屢次運行程序將產生不一樣的結果,並且給定的任何一次運行的結果都不可預知。服務器

爭用條件的一個簡單例子是遞增一個字段。假定某個類有一個私有 static 字段(在 Visual Basic 中爲 Shared),每建立該類的一個實例時它都遞增一次,使用的代碼是 objCt++; (C#) 或 objCt += 1 (Visual Basic)。此操做要求將 objCt 中的值加載到一個寄存器中,使該值遞增,而後將其存儲到 objCt 中。多線程

在多線程應用程序中,一個已加載並遞增該值的線程可能會被另外一個線程搶先,搶先的線程執行所有的三個步驟;當第一個線程繼續執行並存儲其值時,它改寫 objCt,但不考慮該值在它暫停執行期間已更改這一事實。併發

這種爭用條件經過使用 Interlocked 類的方法,如 Interlocked.Increment,便可輕鬆避免。若要了解在多個線程間同步數據的其餘技巧,請參見爲多線程處理同步數據函數

爭用條件也可能會在同步多個線程的活動時發生。編寫每一行代碼,都必須考慮出現如下特殊狀況時會發生什麼狀況,這裏的特殊狀況是指:一個線程在執行該行代碼(或構成該行的任何機器指令)前,其餘線程搶先執行了該代碼。工具

處理器數目

多線程編程技術可以解決單處理器計算機和多處理器計算機的諸多問題,單處理器計算機大多用來運行最終用戶軟件,多處理器計算機一般用做服務器。

單處理器計算機

多線程編程爲計算機用戶提供了更好的響應能力,而且使用空閒時間處理後臺任務。若是在單處理器計算機上使用多線程編程,那麼:

  • 在任什麼時候刻都只有一個線程在運行。

  • 後臺線程僅在主用戶線程空閒時才執行。連續運行的前臺線程將使後臺線程得不處處理器時間。

  • 對一個線程調用 Thread.Start 方法時,此線程只有等到當前線程結束或被操做系統搶佔後纔會執行。

  • 出現爭用條件的緣由一般是,程序員未預見到一個線程可能會在一個難以控制的時刻被搶佔這一事實,有時就會出現另外一線程搶先使用代碼塊這種狀況。

多處理器計算機

多線程編程提供了更大的吞吐量。十個處理器能夠完成一個處理器的十倍的工做量,不過,只有將任務分開並讓十個處理器同時工做才行;線程爲劃分任務並利用額外的處理能力提供了一種方便的辦法。若是在多處理器計算機上使用多線程編程,那麼:

  • 能夠併發執行的線程的數目取決於處理器的數目。

  • 後臺線程只有在正在執行的前臺線程的數目小於處理器的數目時才執行。

  • 當您對一個線程調用 Thread.Start 方法時,此線程可能會,也可能不會當即執行,具體取決於處理器數目和當前在等待執行的線程的數目。

  • 爭用條件不只可能由於線程被意外搶佔而發生,還可能由於在不一樣的處理器上執行的兩個線程在搶用同一代碼塊而發生。

靜態成員和靜態構造函數

在類的類構造函數(C# 中的 static 構造函數、Visual Basic 中的 Shared Sub New)完成運行以前,該類不會初始化。爲防止對未初始化的類型執行代碼,在類構造函數完成運行以前,公共語言運行庫會禁止從其餘線程到類的 static 成員(Visual Basic 中的 Shared 成員)的全部調用。

例如,若是某個類構造函數啓動了一個新線程,而且該線程過程調用了該類的 static 成員,則在該類構造函數完成以前,會一直禁止新線程。

以上狀況適用於可擁有 static 構造函數的任意類型。

通常性建議

使用多線程時要考慮如下準則:

  • 不要使用 Thread.Abort 終止其餘線程。對另外一個線程調用 Abort 無異於引起該線程的異常,也不知道該線程已處理到哪一個位置。

  • 不要使用 Thread.Suspend 和 Thread.Resume 來同步多個線程的活動。不要使用 MutexManualResetEventAutoResetEvent 和 Monitor

  • 不要從主程序中控制輔助線程的執行(如使用事件),而應在設計程序時讓輔助線程負責等待任務,執行任務,並在完成時通知程序的其餘部分。若是輔助線程不阻止,請考慮使用線程池線程。Monitor.PulseAll 在輔助線程阻止的狀況下會頗有用。

  • 不要將類型用做鎖定對象。例如,避免在 C# 中使用 lock(typeof(X)) 代碼,或在 Visual Basic 中使用 SyncLock(GetType(X)) 代碼,或將 System.Threading.Monitor.Enter(System.Object)和 Type 對象一塊兒使用。對於給定類型,每一個應用程序域只有一個 System.Type 實例。若是您鎖定的對象的類型是 public,您的代碼以外的代碼也可鎖定它,但會致使死鎖。有關其餘信息,請參見可靠性最佳作法

  • 鎖定實例時要謹慎,例如,C# 中的 lock(this) 或 Visual Basic 中的 SyncLock(Me)。若是您的應用程序中不屬於該類型的其餘代碼鎖定了該對象,則會發生死鎖。

  • 必定要確保已進入監視器的線程始終離開該監視器,即便當線程在監視器中時發生異常也是如此。C# 的 lock 語句和 Visual Basic 的 SyncLock 語句可自動提供此行爲,它們用一個 finally 塊來確保調用 Monitor.Exit。若是沒法確保調用 Exit,請考慮將您的設計更改成使用 Mutex。Mutex 在當前擁有它的線程終止後會自動釋放。

  • 必定要針對那些須要不一樣資源的任務使用多線程,避免向單個資源指定多個線程。例如,任何涉及 I/O 的任務都會從其擁有其本身的線程這一點獲得好處,由於此線程在 I/O 操做期間將阻止,從而容許其餘線程執行。用戶輸入是另外一種可從專用線程獲益的資源。在單處理器計算機上,涉及大量計算的任務可與用戶輸入和涉及 I/O 的任務並存,但多個計算量大的任務將相互競爭。

  • 對於簡單的狀態更改,請考慮使用 Interlocked 類的方法,而不是 lock 語句(在 Visual Basic 中爲 SyncLock)。lock 語句是一個優秀的通用工具,可是 Interlocked 類爲必須是原子性的更新提供了更好的性能。若是沒有爭奪,它會在內部執行一個鎖定前綴。在查看代碼時,請注意相似於如下示例所示的代碼。在第一個示例中,狀態變量是遞增的:

    lock(lockObject) 
    {
        myField++;
    }

    可使用 Increment 方法代替 lock 語句,從而提升性能,以下所示:

    System.Threading.Interlocked.Increment(myField);
     
    Note注意

    在 .NET Framework 2.0 版中,Add 方法提供增量大於 1 的原子更新。

    在第二個示例中,僅當引用類型變量爲空引用(在 Visual Basic 中爲 Nothing)時,它纔會被更新。

    if (x == null)
    {
        lock (lockObject)
        {
            if (x == null)
            {
                x = y;
            }
        }
    }

    改用 CompareExchange 方法能夠提升性能,以下所示:

    System.Threading.Interlocked.CompareExchange(ref x, y, null);
    Note注意

    在 .NET Framework 2.0 版中,CompareExchange 方法具備一個泛型重載,可用於對任何引用類型進行類型安全的替換。

類庫的建議

在爲多線程編程設計類庫時,請考慮如下準則:

  • 若是可能,請避免同步需求。對於大量使用的代碼更應如此。例如,能夠將一個算法調整爲容忍爭用狀況,而不是徹底消除爭用狀況。沒必要要的同步會下降性能,而且可能致使出現死鎖和爭用狀況。

  • 默認狀況下使靜態數據(在 Visual Basic 中爲 Shared)是線程安全的。

  • 默認狀況下不要使實例數據是線程安全的。經過添加鎖來建立線程安全的代碼的作法會下降性能、加重鎖爭奪,而且可能致使出現死鎖。在常見的應用程序模型中,某一時刻只有一個線程執行用戶代碼,這樣可使對線程安全的需求變爲最小。出於此緣由,.NET Framework 類庫默認狀況下不是線程安全的。

  • 避免提供可更改靜態狀態的靜態方法。在常見的服務器方案中,靜態狀態在各個請求之間是共享的,這意味着多個線程可在同一時刻執行該代碼。這樣就有可能出現線程錯誤。請考慮使用一種設計模式,將數據封裝到在各請求之間不共享的實例中。此外,若是同步靜態數據,更改狀態的靜態方法間的調用可致使死鎖或冗餘同步,從而下降性能。

相關文章
相關標籤/搜索