內容預告:
- 線程入門(線程概念,創建線程)
- 同步基礎(同步本質,線程安全,線程中斷,線程狀態,同步上下文)
- 使用線程(后台任務,線程池,讀寫鎖,異步代理,定時器,本地存儲)
- 高級話題(非阻塞線程,扶起和恢復)
同步的本質:下面的列表總結了.NET同步線程的工具:
阻塞函數:
- Sleep:阻塞線程一定時間。
- Join:阻塞另一個線程至本線程完成。
加鎖結構:
- lock:保證只有一個線程可以存取同一個資源,或操作一段代碼。不能跨進程。速度快。
- Mutex:保證只有一個線程可以存取同一個資源,或操作一段代碼。可以用來阻止一個程序啟動多個線程。可以跨進程,速度一般。
- Semaphore:保證不超過某個數量的線程可以存取同一個資源,或操作一段代碼。可以跨進程,速度一般。
信號結構:
- EventWaitHandle:允許一個線程等待,直到收到另一個線程的信號為止。可以跨進程,速度一般。
- Wait 和 Pulse:允許一個線程等待,直到遇到一個自定義阻塞情況。不能跨進程。速度一般。
非阻塞的同步結構:
- Interlocked:執行簡單的非阻塞原子操作。可以跨進程,速度非常快。
- volatile:為了允許安全的非阻塞操作在鎖外部存取單獨的字段。可以跨進程,速度非常快。
阻塞:
當用上面提到的方式讓線程暫停時,叫做讓線程阻塞。一旦線程阻塞了,一個線程立即讓出自己從CPU分配的時間,將WaitSleepJoin加入ThreadState屬性,直到不再阻塞為止。阻塞的解除可能是以下4種方式之一(不算按電源鍵):
- 滿足了阻塞條件
- 超時
- 被Thread.Interrupt打斷
- 被Thread.Abort終止
一個通過Suspend函數暫停的線程不會被阻塞。
休眠和旋轉:
調用Thread.Sleep在給定時間內阻塞當前線程(或直到被打斷):
static void Main() { Thread.Sleep (0); // relinquish CPU time-slice Thread.Sleep (1000); // sleep for 1000 milliseconds Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour Thread.Sleep (Timeout.Infinite); // sleep until interrupted }
更精確地說,Thread.Sleep將控制權歸還給CPU。Thread.Sleep(0)意思是把自己的時間片都讓給其他活躍線程執行。Thread類有一個SpinWait函數,這個函數不放棄任何CPU時間,而是讓CPU地在給定次數的"無效地忙碌"中循環。50個迭代大概等於暫停一微秒左右。從技術上來說,SpinWait不是一個阻塞的方式:一個旋轉-等待的線程不再有WaitSleepJoin的線程狀態,且不能被其他線程打斷。
SpinWait很少用,它主要的目的是等待一個能快速准備好的資源(1微秒)而不調用Sleep和CPU切換線程。雖然這個技術只能用在多核機器上,因為在單核機器上,沒有機會在線程間旋轉其時間片。而且SpinWait本身也很浪費CPU時間。
阻塞和旋轉:
一個線程可以通過顯式地輪詢來等待:比如:while (!proceed);或while (DateTime.Now < nextStartTime);這種操作非常浪費CPU時間,線程在這種情況下不算阻塞,不像線程等待EventWaitHandle。變量有時在阻塞和旋轉間混合:
while (!proceed) Thread.Sleep (x); // "Spin-Sleeping!"
x越大,CPU用的越多,這樣會增加延遲,但除了延遲,這種組合旋轉和休眠的方式運行的很好。
Join一個線程:
你可以用Join阻塞一個線程,直到另一個線程結束:
class JoinDemo { static void Main() { Thread t = new Thread (delegate() { Console.ReadLine(); }); t.Start(); t.Join(); // Wait until thread t finishes Console.WriteLine ("Thread t's ReadLine complete!"); } }
Join函數接收一個超時函數,毫秒單位,或一個TimeSpan,如果超時了則返回false,帶超時的Joint很像Sleep,其實下面兩行代碼是幾乎 一樣的:
Thread.Sleep (1000); Thread.CurrentThread.Join (1000);
語義上的區別在於Join的意思是保持消息泵在阻塞時仍然活躍。Sleep暫停消息泵。
鎖和線程安全:
只有一個線程可以鎖住同步對象,如果有多個線程的話,那么其他線程會處在排隊狀態,並且是先到先得的原則。C#的lock其實相當於是Moniter.Enter和Moniter.Leave在try/catch語句里的簡化。
class ThreadSafe { static object locker = new object(); static int val1, val2; static void Go() { lock (locker) { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } } }
相當於
try { Monitor.Enter (locker); if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } finally { Monitor.Exit (locker); }
調用Monitor.Exit之前沒有調用Monitor.Enter會拋出異常。Moniter還有一個TryEnter的函數可以指定一個超時時間,等當前線程超時后進入臨界區。
選擇一下同步對象:同步對象必須是引用類型,而不管你具體是什么引用類型。
嵌套鎖:線程可以重復地鎖同一個對象,可以通過Moniter.Enter和Moniter.Leave也可以通過lock。
static object x = new object(); static void Main() { lock (x) { Console.WriteLine ("I have the lock"); Nest(); Console.WriteLine ("I still have the lock"); } Here the lock is released. } static void Nest() { lock (x) { ... } Released the lock? Not quite! }
什么時候加鎖:一個基本原則是只要存在多線程操作一個字段就應該加鎖,哪怕是一個簡單的賦值運算。
性能考上的考慮:加鎖操作本身是很快的,也就幾納秒的事,但是線程間等待的時間可能很長。但如果使用不當會引起:
- 並發枯竭,當加鎖區域的代碼太長,引起其他線程等的太久時可能發生。
- 死鎖,當兩個線程同時等待對方時發生,但都不能繼續執行時。通常是由於同步對象太多造成的。
- 鎖競爭,當兩個線程都有可能先獲得一個鎖,程序可能會因為錯誤的線程獲得了鎖而掛掉。
線程安全:線程安全的代碼是在多線程的場景下沒有不確定性,主要是用鎖,減少線程間的交互來達到目的。
一個函數在任何場合下都線程安全稱之為:可重入的。一般目的的類型很少是線程安全的,因為:
- 保證線程完全安全的開發負擔很重,因為一個類一般有很多個字段。
- 線程安全需要付出性能成本。
- 線程安全的函數不一定需要以線程安全的方式使用。
線程安全是因為通常在多線程場景下開發的需要。有一些方法是用大得復雜的類來達到線程安全的目的。
一種方法是把大段的代碼都包含在一個排它鎖里面。
另一種方法是通過減少共享數據來達到減少線程間的交互,這種方法在無狀態的中間件和網頁服務器上是表現很好的,因為多個客戶端請求的可能同時到達,每個請求都在一個單獨的線程內(像ASP.NET,WEBSERVICE等),那么這些請求必須保證線程安全。無狀態的設計也限制了線程間交互的可能性,因為類不可能在每個請求中保存數據。線程間的交互只限於靜態字段,比如用來用戶共用的內存數據,和提供身份驗證服務。
.NET類型的線程安全:.NET的所有基元類型幾乎都不是線程安全的。可以通過lock把線程不安全的代碼變成線程安全的。
class ThreadSafe { static List <string> list = new List <string>(); static void Main() { new Thread (AddItems).Start(); new Thread (AddItems).Start(); } static void AddItems() { for (int i = 0; i < 100; i++) lock (list) list.Add ("Item " + list.Count); string[] items; lock (list) items = list.ToArray(); foreach (string s in items) Console.WriteLine (s); } }
在這里我們鎖住了list本身,這是可以的,如果有兩個互相交互的list,我們就應該鎖一個通用的對象了。
在這里枚舉list並轉換成Array也不是線程安全的,任何可能對list做修改的操作都不是線程安全的。
靜態成員在.NET里是線程安全的,而實例成員不是。
線程的Interrupt和Abort:可以通過Thread.Interrupt和Thread.Abort來中斷和中止一個線程,但只能活動線程來操作,在等待的線程是無能為力的。
調用Interrupt來強制釋放一個線程,拋出一個線程中斷異常:
class Program { static void Main() { Thread t = new Thread (delegate() { try { Thread.Sleep (Timeout.Infinite); } catch (ThreadInterruptedException) { Console.Write ("Forcibly "); } Console.WriteLine ("Woken!"); }); t.Start(); t.Interrupt(); } }
輸出:Forcibly Woken!
中斷操作一個線程只會讓這個線程從當前時間片釋放等待進入下一個時間片,但不會終止。除非不處理ThreadInterruptedException異常。
如果在沒有加鎖的線程上調用Interrupt,線程會繼續執行直到時間片結束。
隨便中斷一個線程是危險的,因為.NET或者第三方函數在調用棧里可能意外中斷,所以你需要的是在鎖上等待或同步資源。如果函數不是設計的可以中斷的話,可能會引起不安全的代碼以及資源不完全釋放。除非你明確的知道線程的全部細節。
可以用Thread.Abort強制釋放一個線程的時間片,效果和Interrupt差不多,只是這里需要處理的是ThreadAbortException異常,這個異常會在catch塊的尾部被再次拋出,除非在catch里執行Thread.ResetAbort. 線程的狀態變成了AbortRequested。
Interrupt和Abort最大的不同是在線程非阻塞的情況下調用后產生的后果,Interrupt會在下個時間片到來之前繼續執行,而Abort會拋出一個異常。Abort一個非阻塞的線程可能有一些后果,后面會詳細討論。
