首先,肯定的一點:Microsoft的Framework Class Library(FCL)保證了所有靜態方法都是線程安全的。
FCL不保證實例方法是線程安全的。因爲假如全部添加鎖定,會造成性能的巨大損失。另外,假如每個實例方法都需要獲取和釋放一個鎖,事實上會造成最終在任何給定的時刻,你的應用程序只有一個線程在運行,這對性能的影響顯而易見。
下面介紹基元線程同步構造。
基元:是指可以在代碼中使用的最簡單的構造。有兩種基元構造:用戶模式(user-mode)和內核模式(kernel-mode)。
用戶模式
使用了特殊的CPU指令來協調線程。
技術:volatile關鍵字、Interlocked類(互鎖)、spinlock(自旋鎖)
常見鎖①:volatile 關鍵字指示一個字段可以由多個同時執行的線程修改。 聲明爲 volatile 的字段不受編譯器優化(假定由單個線程訪問)的限制。 這樣可以確保該字段在任何時間呈現的都是最新的值。
Interlocked類: 爲多個線程共享的變量提供原子操作。。所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。
常見鎖②:SpinLock 結構是一個低級別的互斥同步基元,它在等待獲取鎖時進行旋轉。在多核計算機上,當等待時間預計較短且極少出現爭用情況時,SpinLock 的性能將高於其他類型的鎖。即使 SpinLock 未獲取鎖,它也會產生線程的時間片。 它這樣做是爲了避免線程優先級別反轉,並使垃圾回收器能夠繼續執行。 在使用 SpinLock 時,請確保任何線程持有鎖的時間不會超過一個非常短的時間段,並確保任何線程在持有鎖時不會阻塞。
優點:
應儘量使用基元用戶模式構造,它們的速度要顯著快於內核模式的構造。
- 協調線程的在硬件中發生的(所以才這麼快)。
- 但是Microsoft Windows操作系統永遠檢測不到一個線程在基元用戶模式的構造上阻塞了。
- 由於在用戶模式的基元構造上阻塞的線程池永遠不認爲已堵塞,所以線程池不會創建新線程來替換這種臨時的線程。
- 這些CPU指令只阻塞線程相當短的時間。
缺點:
- 只有Windows操作系統內核才能停止一個線程的運行(防止它浪費CPU的時間)。
- 在用戶模式中運行的線程可能被系統搶佔,但線程會以最快的速度再次調度。
- 想要取得資源但暫時取不到的線程會一直在用戶模式中“自旋”,這可能浪費大量的CPU時間。線程一直在一個CPU上運行,我們稱爲“活鎖”(livelock)。
實例:
using System;
using System.Threading;
public class Worker
{
// This method is called when the thread is started.
public void DoWork()
{
while (!_shouldStop)
{
Console.WriteLine("Worker thread: working...");
}
Console.WriteLine("Worker thread: terminating gracefully.");
}
public void RequestStop()
{
_shouldStop = true;
}
// Keyword volatile is used as a hint to the compiler that this data
// member is accessed by multiple threads.
private volatile bool _shouldStop;
}
public class WorkerThreadExample
{
static void Main()
{
// Create the worker thread object. This does not start the thread.
Worker workerObject = new Worker();
Thread workerThread = new Thread(workerObject.DoWork);
// Start the worker thread.
workerThread.Start();
Console.WriteLine("Main thread: starting worker thread...");
// Loop until the worker thread activates.
while (!workerThread.IsAlive) ;
// Put the main thread to sleep for 1 millisecond to
// allow the worker thread to do some work.
Thread.Sleep(1);
// Request that the worker thread stop itself.
workerObject.RequestStop();
// Use the Thread.Join method to block the current thread
// until the object's thread terminates.
workerThread.Join();
Console.WriteLine("Main thread: worker thread has terminated.");
}
// Sample output:
// Main thread: starting worker thread...
// Worker thread: working...
// Worker thread: working...
// Worker thread: working...
// Worker thread: working...
// Worker thread: working...
// Worker thread: working...
// Worker thread: terminating gracefully.
// Main thread: worker thread has terminated.
}
內核模式
由Windows操作系統自身提供的。它們要求在應用程序的線程中調用有操作系統內核實現的函數。
技術:EventWaitHandle(事件)、Semaphore(信號量)、Mutex(互斥體)
System.Object
System.MarshalByRefObject
System.Threading.WaitHandle
System.Threading.EventWaitHandle
System.Threading.Mutex
System.Threading.Semaphore
常見鎖③:Mutex 類是 Win32 構造的包裝,它可以跨應用程序域邊界進行封送處理,可用於多個等待,並且可用於同步不同進程中的線程。
優點:
- 線程通過內核模式的構造獲取其他線程擁有的資源時,Windows會阻塞線程以避免它浪費CPU時間。當資源變得可用時,Windows會恢復線程,允許它訪問資源。它不會佔着一個CPU“自旋”。
- 可實現本機和託管線程相互之間的同步。
- 可同步在同一臺機器的不同進程中運行的線程。
- 可應用安全性設置,防止未經授權的賬戶訪問它們。
- 線程可一直阻塞,直到及合作的所有內核模式構造都可用,或者直到集合中的任何內核模式構造可用。
- 在內核模式的構造上阻塞的線程可指定超時值:指定時間內訪問不到希望的資源,線程就可以解除阻塞並執行其他任務。
缺點:
將線程從用戶模式切換爲內核模式(或者相反)會招致巨大的性能損失,這正是爲什麼要避免使用內核構造的原因。另外,線程一直阻塞,會導致“死鎖“(deadlock)。
死鎖總是由於活鎖,因爲活鎖即浪費CPU時間,有浪費內存(線程棧等),而死鎖只浪費內存。
混合構造
兼具上面兩者的長處。在沒有競爭的情況下,快而且不會阻塞(就像用戶模式)。在有競爭的情況,希望它被操作系統內核阻塞。
技術:ManualResetEventSlim類、SemaphoreSlim類、Monitor類、Lock類、ReaderWriterLockSlim類、CountdownEvent類、Barrier類、雙檢鎖.
常見鎖④:Monitor 通常更爲可取,因爲監視器是專門爲 .NET Framework 而設計的,因而它比Mutex可以更好地利用資源。儘管 mutex 比監視器更爲強大,但是相對於 Monitor 類,它所需要的互操作轉換更消耗計算資源。
常見鎖⑤:使用 lock (C#) 或 SyncLock (Visual Basic) 關鍵字是Monitor的封裝。通常比直接使用 Monitor 類更可取,一方面是因爲 lock 或 SyncLock 更簡潔,另一方面是因爲lock 或 SyncLock 確保了即使受保護的代碼引發異常,也可以釋放基礎監視器。
常見鎖⑥:ReaderWriterLock 鎖,在某些情況下,可能希望只在寫入數據時鎖定資源,在不更新數據時允許多個客戶端同時讀取數據。 ReaderWriterLock 類在線程修改資源時將強制其獨佔訪問資源,但在讀取資源時則允許非獨佔訪問。 ReaderWriter 鎖可用於代替排它鎖。使用排它鎖時,即使其他線程不需要更新數據,也會讓這些線程等待。
雙檢鎖
常見鎖⑦:雙重檢查鎖定模式(也被稱爲”雙重檢查加鎖優化”,”鎖暗示”(Lock hint)) 是一種軟件設計模式用來減少併發系統中競爭和同步的開銷。
雙重檢查鎖定模式首先驗證鎖定條件(第一次檢查),只有通過鎖定條件驗證才真正的進行加鎖邏輯並再次驗證條件(第二次檢查)。
它通常用於減少加鎖開銷,尤其是爲多線程環境中的單例模式實現“惰性初始化”。惰性初始化的意思是直到第一次訪問時才初始化它的值。
public sealed class Singleton
{
private static volatile Singleton instance = null;
private static object syncRoot = new Object();
private static int count = 100;
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
public static Singleton GetSingleton()
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
public override string ToString()
{
lock (syncRoot)
{
int buf = --count;
Thread.Sleep(20);
return buf + "_" + count.ToString();
}
}
}
static void Main(string[] args)
{
Task<string>[] tasks = new Task<string>[10];
Stopwatch watch = new Stopwatch();
object syncRoot = new Object();
watch.Start();
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Factory.StartNew<string>(() =>(Singleton.GetSingleton().ToString()));
}
Task.WaitAll(tasks, 5000);//設置超時5s
watch.Stop();
Console.WriteLine("Tasks running need " + watch.ElapsedMilliseconds + " ms." + "\n");
for (int i = 0; i < tasks.Length; i++)
{
//超時處理
if (tasks[i].Status != TaskStatus.RanToCompletion)
{
Console.WriteLine("Task {0} Error!", i + 1);
}
else
{
//save result
Console.WriteLine("Tick ID is " + tasks[i].Result);
Console.WriteLine();
}
}
for (int i = 0; i < 3; i++)
{
Console.WriteLine("Main thread do work!");
Thread.Sleep(200);
}
Console.ReadKey();
}
輸出:
Tasks running need 298 ms.
Tick ID is 96_96
Tick ID is 99_99
Tick ID is 97_97
Tick ID is 98_98
Tick ID is 95_95
Tick ID is 94_94
Tick ID is 93_93
Tick ID is 92_92
Tick ID is 91_91
Tick ID is 90_90
Main thread do work!
Main thread do work!
Main thread do work!