共用方式為


受控線程最佳做法

多線程需要仔細的程序設計。 針對大部分的工作,您可以將線程集區線程執行的要求排入佇列,以減少複雜性。 本主題解決較困難的情況,例如協調多個線程的工作,或處理封鎖的線程。

備註

從 .NET Framework 4 開始,工作平行連結庫和 PLINQ 會提供 API,以減少多線程程式設計的某些複雜度和風險。 如需詳細資訊,請參閱 .NET 中的平行程序設計

死結和競爭條件

多線程可解決輸送量和回應性的問題,但這樣做會產生新的問題:死結和競爭條件。

死結

當兩個線程嘗試鎖定另一個線程已鎖定的資源時,就會發生死結。 兩個線程都無法進行任何進一步的進度。

受管控線程類別的許多方法都會提供超時設定,以協助您偵測死結。 例如,下列程式碼會嘗試取得名為 lockObject 的物件的鎖。 如果在 300 毫秒內未取得鎖定,Monitor.TryEnter 則會傳回 false

If Monitor.TryEnter(lockObject, 300) Then
    Try
        ' Place code protected by the Monitor here.
    Finally
        Monitor.Exit(lockObject)
    End Try
Else
    ' Code to execute if the attempt times out.
End If
if (Monitor.TryEnter(lockObject, 300)) {
    try {
        // Place code protected by the Monitor here.
    }
    finally {
        Monitor.Exit(lockObject);
    }
}
else {
    // Code to execute if the attempt times out.
}

比賽條件

競爭條件是當程序的結果相依於兩個或多個線程中的哪一個先到達特定程式代碼區塊時,就會發生錯誤。 執行程式多次會產生不同的結果,而且無法預測任何指定執行的結果。

競爭條件的簡單範例是將欄位遞增。 假設一個類別有一個私用的靜態欄位(在 Visual Basic 中稱為共用),每次建立類別的實例時,此欄位會自動遞增,這通常使用類似objCt++;(C#)或objCt += 1(Visual Basic)的程式碼來實作。 這項作業需要將值從 objCt 載入緩存器、遞增值,並將其儲存在 中 objCt

在多線程應用程式中,已載入和遞增值的線程,可能會由另一個執行這三個步驟的線程先佔;當第一個線程繼續執行並儲存其值時,它會覆寫 objCt 而不考慮值在過渡期間變更的事實。

很容易使用 Interlocked 類別的方法,例如 Interlocked.Increment,來避免這種特定的競速條件。 若要閱讀在多個線程之間同步處理數據的其他技術,請參閱 同步處理多線程的數據

當您同步處理多個執行緒的活動時,也會發生競爭狀況。 每當您撰寫一行程式碼時,必須考慮如果一個線程在執行該行或任何構成該行的機器指令之前被搶先執行,會發生什麼情況,以及另一個線程超前執行的情況。

靜態成員和靜態建構函式

在類別建構函式(C# 中的 static,在 Visual Basic 中的 Shared Sub New)完成執行之前,類別不會被初始化。 為了防止在未初始化的類型上執行程式碼,Common Language Runtime 會封鎖其他線程 static 對類別成員的所有呼叫(Shared Visual Basic 中的成員),直到類別建構函式完成執行為止。

例如,如果類別建構函式啟動新的線程,而線程過程會呼叫 static 類別的成員,新的線程會封鎖直到類別建構函式完成為止。

這適用於可具有 static 建構函式的任何類型。

處理器數目

系統上有多個處理器或只有一個處理器會影響多線程架構。 如需詳細資訊,請參閱 處理器數目

利用此 Environment.ProcessorCount 特性來判斷執行時可用的處理器數量。

一般建議

使用多個線程時,請考慮下列指導方針:

  • 請勿使用 Thread.Abort 來終止其他線程。 在另一個線程上呼叫 Abort ,類似於在該線程上擲回例外狀況,而不知道線程在其處理中達到什麼點。

  • 請勿使用 Thread.SuspendThread.Resume 來同步處理多個線程的活動。 請使用 MutexManualResetEventAutoResetEventMonitor

  • 請勿從主要程式控制工作線程執行(例如使用事件)。 請設計您的程式,讓工作執行緒負責等待工作可供執行,然後執行並在完成後通知程式的其他部分。 如果您的工作執行緒沒有阻塞,請考慮使用執行緒池執行緒。 Monitor.PulseAll 在工作線程封鎖的情況下很有用。

  • 請勿使用型別作為鎖定的對象。 也就是說,請避免在 C# 中使用 lock(typeof(X)) 程式碼,或在 Visual Basic 中使用 SyncLock(GetType(X)) 程式碼,或將 Monitor.Enter 用於 Type 物件。 針對指定的類型,每個應用程式域只有一個 實例 System.Type 。 如果您鎖定的型別是公有的,那麼您自己以外的程式碼也可能鎖定它,導致死結。 如需其他問題,請參閱 可靠性最佳做法

  • 在鎖定實例時請小心,例如在 C# 的 lock(this) 或 Visual Basic 的 SyncLock(Me) 中。 如果應用程式中其他的程式碼在該類型以外鎖定該對象,就可能會發生死結。

  • 請務必確定已進入監視器的線程一律會離開該監視器,即使線程在監視器中發生例外狀況也一樣。 C# lock 語句和 Visual Basic SyncLock 語句會自動提供此行為,並採用 finally 區塊來確保 Monitor.Exit 已呼叫。 如果您無法確定會呼叫 Exit ,請考慮將設計變更為使用 Mutex。 當目前擁有 Mutex 的線程終止時,會自動釋放 Mutex。

  • 請針對需要不同資源的工作使用多個線程,並避免將多個線程指派給單一資源。 例如,任何涉及 I/O 的工作都受益於有自己的線程,因為該線程會在 I/O 作業期間封鎖,因而允許其他線程執行。 用戶輸入是另一個受益於專用線程的資源。 在單處理器計算機上,牽涉到密集計算的工作會與使用者輸入並存,以及涉及 I/O 的工作,但多個計算密集型工作彼此競爭。

  • 請考慮使用 類別的方法 Interlocked 進行簡單狀態變更,而不是使用 lock 語句 (SyncLock 在 Visual Basic 中)。 陳述 lock 是很好的一般用途工具,但 Interlocked 類在必須是原子性的更新中提供更佳的效能。 在內部,如果沒有爭用,它會執行單一鎖定前置詞。 在程式代碼檢閱中,監看如下列範例所示的程序代碼。 在第一個範例中,狀態變數會遞增:

    SyncLock lockObject
        myField += 1
    End SyncLock
    
    lock(lockObject)
    {
        myField++;
    }
    

    您可以使用 Increment 方法來改善效能,而不是使用 lock 語句,如下所示:

    System.Threading.Interlocked.Increment(myField)
    
    System.Threading.Interlocked.Increment(myField);
    

    備註

    使用 Add 方法對大於 1 的原子遞增進行操作。

    在第二個範例中,只有在參考類型變數是 Null 參考時才會更新 (Nothing 在 Visual Basic 中)。

    If x Is Nothing Then
        SyncLock lockObject
            If x Is Nothing Then
                x = y
            End If
        End SyncLock
    End If
    
    if (x == null)
    {
        lock (lockObject)
        {
            x ??= y;
        }
    }
    

    您可以改用 CompareExchange 方法來改善效能,如下所示:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);
    

    備註

    CompareExchange<T>(T, T, T)方法多載會提供一種針對參考型別的型別安全式替代方案。

類別庫的建議

設計多線程的類別庫時,請考慮下列指導方針:

  • 盡可能避免同步處理的需求。 這特別適用於大量使用的程序代碼。 例如,可能會調整演算法以容許競爭條件,而不是加以消除。 不必要的同步處理會降低效能,併產生死結和競爭條件的可能性。

  • 根據預設,讓靜態資料(Shared 使用於 Visual Basic)執行緒安全。

  • 根據預設,請勿讓實例數據線程安全。 新增鎖定來建立線程安全的程式碼會降低效能、提高鎖爭用,並增加死結發生的可能性。 在常見的應用程式模型中,一次只有一個線程會執行用戶程序代碼,這可將線程安全性的需求降到最低。 因此,根據預設,.NET 類別庫不是安全線程。

  • 避免提供改變靜態狀態的靜態方法。 在常見的伺服器案例中,靜態狀態會在要求之間共用,這表示多個線程可以同時執行該程序代碼。 這會增加執行緒錯誤的可能性。 請考慮使用設計模式,將數據封裝成未在請求之間共用的實體。 此外,如果同步處理靜態數據,則改變狀態的靜態方法之間的呼叫可能會導致死結或重複同步處理,對效能造成負面影響。

另請參閱