CLR 基元線程同步構造web
《CLR via C#》到了最後一部分,這一章重點在於線程同步,多個線程同時訪問共享數據時,線程同步能防止數據雖壞。之因此要強調同時,是由於線程同步問題其實就是計時問題。爲構建可伸縮的、響應靈敏的應用程序,關鍵在於不要阻塞你擁有的線程,使它們能用於(和重用於)執行其餘任務。編程
不須要線程同步是最理想的狀況,由於線程同步存在許多問題:緩存
1 第一個問題是,它比較繁瑣,很容易出錯。安全
2 第二個問題是,它們會損壞性能。獲取和釋放鎖是須要時間的,由於要調用一些額外的方法,並且不一樣的CPU 必須進行協調,以決定哪一個線程先取得鎖。讓機器中的CPU 以這種方式互相通訊,會對性能形成影響。服務器
添加鎖後速度會慢下來,具體慢多少要取決於所選的鎖的種類。即使是最快的鎖,也會形成 方法 數倍地慢於沒有任何鎖的版本。多線程
3 第三個問題在於,它們一次只容許一個線程訪問資源。這是鎖的所有意義之所在,但也是問題之所在,由於阻塞一個線程會形成更多的線程被建立。併發
線程同步如此的很差,應該如何在設計本身的應用時,儘可能避免線程同步呢?app
具體就是避免使用像靜態字段這樣的共享數據。可試着使用值類型,由於它們老是被複制,每一個線程操做的都是它本身的副本。異步
多個線程同時共享數據進行只讀訪問是沒有任何問題的。async
1 類庫和線程安全
Microsoft 的 Framework Class Library (FCL)保證全部靜態方法都是線程安全的。另外一方面,FCL 不保證明列方法是線程安全的。Jeffery Richter 建議你本身的類庫也遵循這個模式。這個模式有一點要注意:若是實例方法的目的是協調線程,則實例方法應該是線程安全的。
注意:使一個方法線程安全,並非說它必定要在內部獲取一個線程同步鎖。線程安全的方法意味着在兩個線程試圖同時訪問數據時,數據不會被破壞。例如:System.Math 類的一個靜態方法 Max。
2 基元用戶模式和 內核模式構造
基元(primitive)是指能夠在代碼中使用的最簡單的構造。有兩種基元構造:用戶模式(user-mode)和 內核模式(kernel-mode)。儘可能使用基元用戶模式構造,它們的速度要顯著快於內核模式構造。由於它們使用了特殊 CPU 指令來協調線程。這意味着協調是在硬件中發生的(因此才這麼快)。
但這意味着 Windows 系統永遠檢測不到一個線程在基元用戶模式的構造上阻塞了。因爲在用戶模式的基元構造上阻塞的線程池不認爲已阻塞,因此線程池不會建立新的線程來替換這種臨時阻塞的線程。此外,這些CPU 指令只阻塞線程至關短的時間。
3 用戶模式構造
CLR 保證對如下數據類型的變量讀寫是原子性的:Boolean,Char,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single以及引用類型。
舉個列子:
internal static class SomeTyoe{
public static Int32 x = 0;
}
若是一個線程執行這一行代碼:
SomeType.x = 0x01234567;
x 變量會一次性(原子性)地從0x00000000 變成0x01234567。另外一個線程不可能看處處於中間狀態的值。假定上述SomeType 類中的x 字段是一個Int64 ,那麼當一個線程執行如下代碼時:
SomeType.x = 0x0123456789abcdef
另外一個線程可能查詢x ,並獲得0x0123456700000000 或 0x0000000089abcdef 值,由於讀取和寫入操做不是原子性的。
雖然變量的原子訪問可保證讀取或寫入操做一次性完成,但因爲編譯器和CPU 的優化,不保證操做何時發生。本節討論的基元用戶模式構造,用於規劃好這些原子性讀取/寫入操做的時間。 此外,這些構造還可強制對(U)Int64 和 Double 類型的變量進行原子性的、規劃好了時間的訪問。
有兩種基於用戶模式線程同步構造。
1 易變構造:在特定的時間,它在包含一個簡單數據類型的變量上 執行 原子性的讀 或 寫操做。
2 互鎖構造:在特定的時間,它在包含一個簡單數據類型的變量上 執行 原子性的讀 和 寫操做。
全部易變 和 互鎖構造都要求傳遞對包含簡單數據類型的一個變量的引用(內存地址)。
3.1 易變構造 Volatile.Read 和 Volatile.Write
C# 對易變字段的支持
C# 編譯器提供了 volatile 關鍵字,它可應用於如下任何類型的靜態 或 實例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single和 Char。還可將 volatile 關鍵字應用於引用類型的字段,以及基礎類型爲 (S)Byte,(U)Int16,(U)Int32 的任何枚舉字段。
JIT 編譯器確保對易變字段的全部訪問都是易變讀取或寫入的方式執行,沒必要顯示調用 Volatile 的靜態 Read 或 Write 方法。另外,volatile 關鍵字告訴C# 和 JIT 編譯器不將字段緩存到CPU 的寄存器中,確保字段的全部讀寫操做都在 RAM 中進行。
下面是Volatile.Write 方法和 Volatile.Read 方法的使用。
internal sealed class ThreadsSharingData {
private Int32 m_flag = 0;
private Int32 m_value = 0;
// This method is executed by one thread
public void Thread1() {
// Note: 5 must be written to m_value before 1 is written to m_flag
m_value = 5;
Volatile.Write(ref m_flag, 1);
}
// This method is executed by another thread
public void Thread2() {
// Note: m_value must be read after m_flag is read
if (Volatile.Read(ref m_flag) == 1)
Console.WriteLine(m_value);
}
}
Volatile.Write 方法強迫location 中的值在調用時寫入。此外,按照編碼順序,以前的加載和存儲操做必須在調用 Volatile.Write 以前 發生。
Volatile.Read 方法強迫location 中的值在調用時讀取。此外,按照編碼順序,以後的加載和存儲操做必須在調用 Volatile.Read 以後 發生。
C# 對易變字段的支持
爲了簡化編程,C# 編譯器提供了 Volatile 關鍵字,它可應用於如下任何類型的靜態或實例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single 和 Char。還能夠將 Volatile 關鍵字應用於引用類型的字段,以及基礎類型爲(S)Byte,(U)Int16 或 (U)Int32 的任何枚舉字段。
volatile 關鍵字告訴 C# 和 JIT 編譯器不將字段緩存到 CPU 的寄存器中,確保字段的全部讀寫操做都在 RAM 中進行。
用 volatile 引發的很差事情:
如:m_amount = m_amount + m_amount;
//假定m_amount 是類中定義的一個volatile 字段。編譯器必須生成代碼將m_amount 讀入一個寄存器,再把它讀入另外一個寄存器,將兩個寄存器加到一塊兒,再將結果寫回 m_amount 字段。但最簡單的方式是將它的全部位都左移1 位。
另外,C# 不支持以引用的方式將 volatile 字段傳給方法。
3.2 互鎖構造
本節將討論靜態System.Threading.Interlocked 類提供的方法。InterLocked 類中的每一個方法都執行一次原子讀取 以及 寫入操做。此外,Interlocked 的全部方法都創建了完整的內存柵欄(memory fence)。也就是說,調用某個 Interlocked 方法以前的任何變量寫入都在這個InterLocked 方法調用以前執行。而這個調用以後的任何變量讀取都在這個調用以後讀取。
做者很喜歡用 Interlocked 的方法,它們至關快,不阻塞任何線程。
AsyncCoordinator 可協調異步操做。做者給了個例子。
internal sealed class MultiWebRequests {
// This helper class coordinates all the asynchronous operations
private AsyncCoordinator m_ac = new AsyncCoordinator();
// Set of web servers we want to query & their responses (Exception or Int32)
// NOTE: Even though multiple could access this dictionary simultaneously,
// there is no need to synchronize access to it because the keys are
// read•only after construction
private Dictionary<String, Object> m_servers = new Dictionary<String, Object> {
{ "http://Wintellect.com/", null },
{ "http://Microsoft.com/", null },
{ "http://1.1.1.1/", null }
};
public MultiWebRequests(Int32 timeout = Timeout.Infinite) {
// Asynchronously initiate all the requests all at once
var httpClient = new HttpClient();
foreach (var server in m_servers.Keys) {
m_ac.AboutToBegin(1);
httpClient.GetByteArrayAsync(server).
ContinueWith(task => ComputeResult(server, task));
}
// Tell AsyncCoordinator that all operations have been initiated and to call
// AllDone when all operations complete, Cancel is called, or the timeout occurs
m_ac.AllBegun(AllDone, timeout);
}
private void ComputeResult(String server, Task<Byte[]> task) {
Object result;
if (task.Exception != null) {
result = task.Exception.InnerException;
} else {
// Process I/O completion here on thread pool thread(s)
// Put your own compute•intensive algorithm here...
result = task.Result.Length; // This example just returns the length
}
// Save result (exception/sum) and indicate that 1 operation completed
m_servers[server] = result;
m_ac.JustEnded();
}
// Calling this method indicates that the results don't matter anymore
public void Cancel() { m_ac.Cancel(); }
// This method is called after all web servers respond,
// Cancel is called, or the timeout occurs
private void AllDone(CoordinationStatus status) {
switch (status) {
case CoordinationStatus.Cancel:
Console.WriteLine("Operation canceled.");
break;
case CoordinationStatus.Timeout:
Console.WriteLine("Operation timed•out.");
break;
case CoordinationStatus.AllDone:
Console.WriteLine("Operation completed; results below:");
foreach (var server in m_servers) {
Console.Write("{0} ", server.Key);
Object result = server.Value;
if (result is Exception) {
Console.WriteLine("failed due to {0}.", result.GetType().Name);
} else {
Console.WriteLine("returned {0:N0} bytes.", result);
}