執行緒同步處理 (C# 程式設計手冊)
更新:2007 年 11 月
下列章節描述的功能和類別,可用來同步處理在多執行緒應用程式中資源的存取。
在應用程式中使用多執行緒的其中一個優點,就是每個執行緒會同步執行。對於 Windows 應用程式而言,這能讓耗費時間的工作在幕後執行,而應用程式視窗和控制項仍能保持回應能力。針對伺服器應用程式,多執行緒處理則提供了一種能力,可用不同的執行緒處理每個接收到的請求。否則,在先前的請求完全得到滿足之前,每個新的請求都不會獲得服務。
不過,執行緒的非同步性質表示必須協調對資源 (例如,檔案控制代碼、網路連線和記憶體) 的存取。否則兩個或以上的執行緒可能會同時存取相同資源,而每個執行緒都不知道其他執行緒的行動。結果是發生無法預期的資料毀損。
針對在整數數字資料型別上的簡單操作,同步處理執行緒可使用 Interlocked 類別的成員來完成。至於所有其他的資料型別和非執行緒安全的資源,只能使用本主題中的建構來安全執行多執行緒處理。
如需多執行緒應用程式的背景資訊,請參閱:
lock 關鍵字
lock 關鍵字可以用來確保程式碼區塊執行直到完畢,而不受其他執行緒的打斷。為指定物件取得程式碼區塊期間的互斥鎖定,便能完成這一點。
lock 陳述式是以 lock 關鍵字開頭,此關鍵字會提供物件做為引數,並在之後接著一次只能由一個執行緒加以執行的程式碼區塊。例如:
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Function()
{
lock (lockThis)
{
// Access thread-sensitive resources.
}
}
}
提供給 lock 關鍵字的引數必須是依據參考型別的物件,且用來定義鎖定的範圍。在上述範例中,鎖定範圍是限制為此函式,因為在函式外部不存在物件 lockThis 的參考。如果這類參考並不存在,鎖定範圍便會擴充至該物件。嚴格說來,提供給 lock 的物件僅用來唯一識別正由多執行緒共用的資源,所以可能為任意的類別執行個體。不過實際上,此物件通常代表執行緒同步處理所需要的資源。例如,如果容器物件是由多執行緒使用,則該容器可以傳遞至鎖定,在鎖定之後的同步化程式碼區塊將可存取容器。只要其他執行緒在存取容器前鎖定了同一個容器,就可以安全地同步處理對物件的存取。
一般而言,您最好避免鎖定 public 型別,或避免鎖定不在應用程式控制範圍內的物件執行個體。例如,若執行個體可以公開地被存取,lock(this) 可能會產生問題,因為在您控制範圍之外的程式碼可能也會鎖定物件。這樣可能會建立死結狀況,在此狀況下有兩個或以上的執行緒等候同一個物件的釋放。相對於物件,鎖定公用資料型別會造成相同原因的問題。而鎖定常值字串則特別具風險性,因為常值字串是由 Common Language Runtime (CLR) 所「拘留」(interned)。這表示對於整個程式,任何指定之字串常值有一個執行個體,完全相同的物件代表在所有執行緒上、所有執行的應用程式定義域中的常值。因此,不論在應用程式處理序的任何地方,置於相同內容字串上的鎖定將會鎖定應用程式中該字串的所有執行個體。因此,最好鎖定未被拘留的 private 或 protected 成員。某些類別特別為鎖定提供成員。例如,Array 型別提供了 SyncRoot。許多集合型別也提供 SyncRoot 成員。
如需 lock 關鍵字的詳細資訊,請參閱:
監視器
就像 lock 關鍵字一樣,監視器會避免多執行緒同時執行程式碼區塊。Enter 方法能讓一個而且是唯一的執行緒處理下列陳述式;其他執行緒會被阻斷,直到執行的執行緒呼叫 Exit 為止。就好像使用 lock 關鍵字一樣。事實上,lock 關鍵字是使用 Monitor 類別實作。例如:
lock (x)
{
DoSomething();
}
這種做法相當於:
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
一般偏好使用 lock 關鍵字,更勝過直接使用 Monitor 類別,這是因為 lock 更為簡潔,且即使受保護的程式碼擲回例外狀況 (Exception),lock 也會確保釋放基礎監視器。可以使用 finally 關鍵字來完成這一點,不論是否擲回例外狀況,此關鍵字都會執行其關聯的程式碼區塊。
如需監視器的詳細資訊,請參閱監視同步處理技術範例。
同步處理事件和等候控制代碼
使用鎖定或監視器避免執行緒敏感的程式碼區塊同時執行,會非常有用,但這些建構並不允許執行緒和另一個執行緒溝通事件。這需要「同步處理事件」(Synchronization Event),此事件是具有兩種狀態 (發出信號和未發出信號) 其中一種的物件,這些狀態可用來啟動和暫止執行緒。讓執行緒等待未發出信號的同步處理事件會暫止執行緒,將事件狀態變更為發出信號則可以啟動執行緒。如果執行緒嘗試等待已經發出訊號的事件,執行緒便會繼續執行,不會受到延遲。
同步處理事件有兩種,分別是 AutoResetEvent 和 ManualResetEvent。它們唯一的不同之處,是在於 AutoResetEvent 會在啟動執行緒後的任何時間,自動從發出信號變更至未發出信號。相反地,ManualResetEvent 則允許其發出信號的狀態啟動任何數目的執行緒,並只有在呼叫 Reset 方法時才還原成未發出信號的狀態。
藉由呼叫 WaitOne、WaitAny 或 WaitAll 其中一種等候方法,可以讓執行緒等候事件發生。WaitHandle.WaitOne() 會導致執行緒等候,直到單一事件發出信號為止;WaitHandle.WaitAny() 則會阻斷執行緒,直到一個或多個指定的事件變成發出信號為止;而 WaitHandle.WaitAll() 會阻斷執行緒,直到所有指定的事件都成為發出信號狀態為止。當呼叫事件的 Set 方法時,該事件就會變成發出信號。
在下列範例中,會由 Main 函式建立和啟動執行緒。新的執行緒使用 WaitOne 方法等候事件。執行緒會被暫止,直到執行 Main 函式的主要執行緒對事件發出信號為止。一旦事件變成發出信號,就會傳回輔助執行緒。在這種情況下,因為事件只用來進行一個執行緒的啟動,所以不能使用 AutoResetEvent 或 ManualResetEvent 類別。
using System;
using System.Threading;
class ThreadingExample
{
static AutoResetEvent autoEvent;
static void DoWork()
{
Console.WriteLine(" worker thread started, now waiting on event...");
autoEvent.WaitOne();
Console.WriteLine(" worker thread reactivated, now exiting...");
}
static void Main()
{
autoEvent = new AutoResetEvent(false);
Console.WriteLine("main thread starting worker thread...");
Thread t = new Thread(DoWork);
t.Start();
Console.WriteLine("main thread sleeping for 1 second...");
Thread.Sleep(1000);
Console.WriteLine("main thread signaling worker thread...");
autoEvent.Set();
}
}
如需執行緒同步處理事件使用方式的範例,請參閱:
Mutex 物件
"Mutex" 類似於監視器;避免一次有一個以上的執行緒同時執行程式碼區塊。事實上,"Mutex" 的名稱就是「互斥」(Mutually Exclusive) 的縮短形式。然而不同於監視器,Mutex 可以用來跨處理序同步處理執行緒。Mutex 是由 Mutex 類別所表示。
當使用做為處理序間的同步處理時,Mutex 稱為「具名的 Mutex」(Named Mutex),因為它有在另一個應用程式中使用,所以無法由全域或靜態變數共用。必須為它指定名稱,如此兩個應用程式才能存取同一個 mutex 物件。
雖然 mutex 可用做處理序間的執行緒同步處理,但通常還是偏好使用 Monitor,因為監視器是特別為 .NET Framework 設計,所以能對資源有更好的利用。相反地,Mutex 類別是 Win32 建構的包裝函式。Mutex 比監視器還要強大,但它在計算上所需的 Interop 轉換,也比 Monitor 類別所需的轉換耗費更多計算資源。如需使用 mutex 的範例,請參閱 Mutex。