對概念的理解是我們做任何事情的基礎,因此我們從概念開始吧
程序執行順序是按照串行執行的假設:
比如我們讀詩詞,默認從上到下
1.床 前 明 月 光,
2.疑 是 地 上 霜。
3.舉 頭 望 明 月,
4.低 頭 思 故 鄉。
而多線程以后,就很有可能變成
3.舉 頭 望 明 月,
1.床 前 明 月 光,
1.床 前 明 月 光,
4.低 頭 思 故 鄉。
一個列隊中元素的數量必須小於或者等於存儲元素的數組長度。如果這個列隊只是在串行程序中使用,那么只要在所有共有方法的入口點和出口點保持這個不變性就足以保證程序的正確性。
一旦缺乏這種假設存在的前提條件,除非使用了某種特殊的方法,不然也許每行程序也許都要確保並發執行的時候不會出錯,這將會讓事情變得非常復雜。
可能一行代碼被執行多次,導致你假設中的算法流程被破壞。
臨界域是保證程序能串行執行的一種方式。
臨界域(Critical Regiion)的概念:
信號量是規定了一定線程數可以執行的概念,這在實現對集合資源的保護時很有用。記住,信號量不屬於某單一線程。
可以認為臨界域是信號量的一個特例,規定了只有一個線程可以執行,也就是互斥。
信號量和臨界域可以結合起來使用,以后會有例子說明。
常見的同義詞諸如“加鎖/釋放”,“進入/退出","開始/結束"等,同義詞雖多,可是表示的都是一個相同的概念。只要所有執行臨界域代碼的線程都按照一直的方式來訪問數據,那么就可以避免發生數據間競爭的問題。
用偽代碼表示就是:
EnterCriticalRegion();
DoSomeThings();
LeaveCriticalRegion();
有些臨界域可以支持共享模式,例如read/write鎖,這種方式能夠使得多個線程並發的讀取共享數據。
當然臨界域這一概念的實現有很多種算法,我們先不討論這個。因為操作系統對臨界域這一概念有不少的既有實現。
不過相應的需求如下:
1.保證互斥性。
2.在臨界域中的操作要進得來也要出得去。不能有線程因為死鎖或者活鎖問題導致無限期的停留在臨界域中。
3.提供某種程度的公平性。
4.最好實現是低開銷的。因為底層系統會頻繁地使用臨界域,進進出出。
可重入函數:
重入表示在一次函數執行的過程中,沒有執行完時,又再次進入同一函數。沖入現象可能由同一線程造成,比如說遞歸;也有可能由多線程並發調用函數造成。
一個函數可重入表示這個函數的結果不會因為重入而產生變化,是穩定的。
粒度問題:
a++是用來說明多線程容易造成問題的最常用的例子。但是實現這種低級別的同步還是相對簡單的,很多cpu內置提供支持。
但是更高級別的同步,比如說一個對象方法,包含很多個步驟,那么要保證其線程安全,就沒那么簡單。
在程序中通常包含一組子系統以及各種復合的數據結構,並且這些數據結構可能被多個線程並發訪問。有兩種方式來組織臨界域:
粗粒度:通過只使用一個鎖來保護子系統以及復合數據結構中的各個部分。優點:易於管理使用。缺點:伸縮性不強。
細粒度:對每一部分分別加鎖。優點:高伸縮高並發。缺點:鎖太多,難以合理划分和組織。誤用時會產生很多問題。
線程:比喻為執行函數的虛擬處理器。
線程的狀態:
1.假設線程沒有狀態的情況下,采用最簡單也是最直觀的“忙等待”(自旋)方式。下面這段程序中謂詞(predicate ,也就是判斷條件)保護了DoSomething的執行。
while(!p)
DoSomething();
直到p為true,也就是獲得進入臨界區的資格后,DoSomething才執行。否則就一直原地打轉,不停的檢查P的值。
但是忙等待依然消耗CPU的運行周期,直到它的時間片用完或者系統搶占把cpu資源分配到其他線程。在自旋過程中會阻止了p為true時其他線程的運行,也就是說已經進入臨界區准備執行DoSomething的線程也必須等待該自旋線程釋放CPU資源。這個執行DoSomething的線程估計會郁悶,心想終於輪到我執行了,CPU運行周期卻被自旋線程用來不停對P求值了。
因為把CPU周期浪費在了不停檢查p這個共享內存的狀態上,所以忙等待在大部分情況下都不是一種好的做法。這種大量使用CPU時鍾周期和執行內存訪問的操作,會導致頻繁的總線通信以及能源消耗(特別是對於移動設備)
所以我們需要其他的手段來表示線程等待,最好干脆就是操作系統幫我們封裝好了。這樣我們只要在對線程說"hold 住",該線程就變成等待狀態,不再占用CPU運行周期。
系統內置對線程狀態的支持:
Windows操作系統通過各種內核對象來提供真正的等待功能。
當線程等待時,它將進入等待狀態(與運行狀態相對應),這將觸發上下文切換操作以將這個線程立即從這個處理器上移走,並且確保Windows線程調度器不會將它作為下一個將要運行的線程。這避免了CPU計算能力以及能源的 浪費,並允許系統中其他線程的執行。假設有系統函數Wait,這個函數可以使得線程進入等待狀態,上面的忙等待代碼就變成
if(!p)
Wati();
DoSomeThing();
現在,臨界域中的線程不僅要使得P變為true,而且必須要考慮其他線程也可能處於等待狀態。用一個WakeUp方法喚醒等待中的一個或多個線程
p = true;
WakeUP();
線程安全方面:
數據的狀態
在面向對象的編程系統中,一個典型的對象由保存狀態的字段和操作狀態的方法組成,狀態被破壞意味着會產生不可預計的后果。
1.共享狀態
當狀態被共享時,多個線程對狀態的並發訪問將會在時間上發生重疊;當這些線程在訪問共享狀態發生重疊時,那么彼此之間的操作將會相互干擾。
.NET框架的類型安全在一定程度上保證了私有狀態,因為如果程序中能夠生成一個指向進程地址空間中任意位置的指針,那么整個地址空間中的數據都是共享狀態的。
共享狀態具有可傳遞性。
new 出來的對象只有創建該對象的線程可以訪問,所以是線程安全的,不過一但被共享狀態所引用(如靜態變量),那么就不再是線程安全的。
2.私有狀態
方法棧,私有變量,參數
5.readonly一定程度上保障了數據的不可變性。雖然可以多次賦值。