計算機的高速發展,在多核技術上要遠遠快於提升單核的計算能力。因而設計並發的程序成為提高軟件性能的一大利器。
並發的程序雖然可以有效利用硬件資源,但同時也會增加程序設計的難度,其首要解決的就是同步的問題。
同步問題歸納而言就是要解決兩個問題:活性失敗(liveness failure)和 安全性失敗(safety failture)。
- 活性失敗是指,線程A操作的變量c,在線程B中要訪問的時候,不是最新的線程A操作賦值后的值。產生此類問題的原因在於現代CPU多采用了高速緩存,高速緩存變成了CPU和內存的中間橋梁,數據的過渡器,而CPU對高速緩存中的數據的修改並不會第一時間刷新到公用的內存中;多個線程運行在不同的CPU的情況下,就有可能出現讀取的數據的不新鮮,導致活性失敗。
- 安全性失敗,舉例來說,線程A在調用某個計算方法, 運算過程處於中間狀態時,有參與運算的變量被其他線程修改了值,導致了線程A的這次運算結果錯誤。這里的問題在於,線程A的這次運算不是一個原子操作,無法保證在運算的整體過程中數據的可控性。此類問題被稱作安全性失敗。
解決的辦法歸納起來也是兩大類
- 程序設計時,盡量減少跨線程的數據交互,盡量設計可重入的計算方法。
- 使用各種編程語言提供的同步機制,保證數據在多個線程之間的正確同步。
對於第一點來說,無論你是用哪種語言,都是一樣的。
那么,針對第二點,讓我們來看一下C#和Java在處理同步上的一些大同小異。
解決活性失敗,C#和Java都提供了volatile這個關鍵字來修飾變量。這個關鍵字可以讓程序運行時對被修飾的變量無條件的在高速緩存和主內存中實現數據同步。使用這個關鍵字可以解決數據不新鮮的問題,但是切記不可亂用,因為它會帶來額外的性能開銷,讓高速緩存變得沒有意義。
解決活性失敗,當然也可以使用各種同步機制,這些同步機制也可以讓需要在一起完成的操作不被其他線程打擾,成為原子操作,從而解決安全性失敗問題。
同步大體來說可以分為兩種
- 互斥同步。互斥同步是指,線程A在訪問某個競爭資源的時候,其他線程不能訪問這個資源而被阻塞。這種方案帶來的問題是比較大的性能開銷用於線程阻塞和喚醒。這種同步機制其實是一種悲觀的同步方案,在操作開始前就假設會有其他線程來搶資源而上鎖了。
- 非阻塞同步。這種同步機制是借助了操作和沖突檢測的硬件指令實現的原子操作,實現的樂觀同步機制。通俗的說,就是先進行操作,如果沒有其他線程在征用共享數據,那操作就成功了,如果產生了沖突,那就不斷重試,直到資源被釋放。非阻塞同步不會讓線程掛起,不需要被喚醒,所以如果在共享資源被短期暫用的情況下,比互斥(阻塞)同步擁有更好的性能。
在C#和Java中,比較典型的非阻塞同步機制就是自旋鎖。
而同步機制的實現機制則是五花八門。
由於C#主要應用的平台在於windows,所以基本上它的同步機制都是基於windows的一些同步原語,包括事件,互斥鎖,信號量,監視器;當然也有優化后的讀寫鎖,瘦鎖等等。
Java由於是跨平台的,所以提供的同步機制都需要jvm支持。可供選擇的同步機制和封裝有synchronized, ReentrantLock,CountDownLatch, CyclicBarrier, DelayQueue, PriorityBlockngQueue, ScheduledExecutor, Semaphore, Exchanger.
后續我們單獨對每種實現進行相應的比較。