一什麼是線程沖突
線程沖突其實就是指兩個或以上的線程同時對同一個共享資源進行操作而造成的問題
一個比較經典的例子是用一個全局變量做計數器然後開N個線程去完成某個任務每個線程完成一次任務就將計數器加一直到完成次任務如果不考慮線程沖突問題用類似下面的代碼去做則很可能會超額完成任務線程越多完成任務次數超出次的可能性就越大
偽代碼如下
int count = ;//全局計數器
void ThreadMethod()//運行在每個線程的方法
{
while( true )
{
if ( count >= )//如果達到任務指標
break;//中斷線程執行
DoSomething();//完成某個任務
count++;
}
}
//省略線程的創建等代碼
具體的為什麼會超額完成任務的原因在這裡我就不贅述了這個例子在單線程環境中是絕對不會超額完成任務的
當然在這個例子中將count++放到if語句中也許能降低一些事故發生的概率但那不是絕對的換言之這樣的程序不能杜絕超額完成任務的可能
其實從線程沖突的定義中我們不難發現要造成線程沖突有兩個必要條件多線程和共享資源這兩個條件中有一個不成立就不可能發生線程沖突問題
所以在單線程環境中是不存在線程沖突的問題的不過很可惜的是我們的軟件早已進化到了多進程多線程的時代單線程的程序幾乎是不存在的無論是WinForm還是WebForm程序運行的環境都是多線程的而不論你自己是不是明確的開啟了一個線程
既然多線程是不可避免的那麼要避免線程沖突就只能從共享資源來開刀了
二線程安全的資源
如果大家經常看MSDN或者VS幫助中的NET類庫參考的話就不難發現幾乎所有的類型都有這麼一句話的描述此類型的任何公共 static(在 Visual Basic中為 Shared) 成員都是線程安全的但不保證所有實例成員都是線程安全的那麼線程安全到底是什麼意思?
其實線程安全很簡單就是指一個函數(方法屬性字段或者別的)在同一時間被不同線程使用不會造成任何線程沖突的問題就說這個東西是線程安全的
接下來來談談什麼樣的資源是線程安全的
之所以使用資源這個詞是因為線程沖突不僅僅會發生在共享的變量上兩個線程同時對同一個文件進行讀寫兩個程序同時用同一個端口與同一個地址進行通信都會造成線程沖突只不過是操作系統和幫我們協調了這些沖突而已
一個線程安全的資源即是指在不同線程中使用不會導致線程沖突問題的資源
一個不能被改變的資源是線程安全的比如說一個常量
const decimal pai = ;
//C++: const double pai = ;
因為pai的值不可能被改變所以在不同的線程中使用也不會造成沖突換言之它在不同的線程中同時被使用和在一個線程中被使用是沒有區別的所以這個東西是線程安全的
同樣的在NET中一個字符串的實例也是線程安全的因為字符串的實例在NET中也是不可以被改變的一個字符串的實例一旦被創建對其所有的屬性方法調用的結果都是唯一確定的永遠不會改變的所以NET類庫參考中String類型才有此類型是線程安全的與之類似的Type類型Assembly類型都是線程安全的
但string的實例是線程安全的卻不代表string的變量是線程安全的換言之假設有一個靜態變量
public static string str = ;
str不是線程安全的因為str這個變量的字符串實例可以被任何線程修改
再考慮這樣的例子
public static readonly SqlConnection connection = new SqlConnection( connectionString );
雖然connection本身雖然是線程安全的但connection的任何成員都不是線程安全的
比如說我在一個線程中對這個connection調用了Open方法然後進行查詢操作但在同一時刻另一個線程調用了Close方法這時候就出現錯誤了
但單純的使用connection而不使用其任何成員比如說if ( connection != null )這樣的代碼是不存在線程沖突的
線程安全的資源其實還有很多在此不一一贅述
對於NET Framework的類型的成員來說只讀的字段是線程安全的
那麼對於屬性和方法來說怎麼知道是不是線程安全的?
三線程安全的函數
因為屬性和方法都是函數組成的所以我們探討一下什麼是線程安全的函數
上面我們說到線程沖突的必要條件是多線程和共享資源那麼如果一個函數裡面沒有使用任何可能共享的資源那麼就不可能出現線程沖突也就是線程安全的比如說這樣的函數
public static int Add( int a int b )
{
return a + b;
}
這個函數中所使用的所有的資源都是自己的局部變量而函數的局部變量是儲存在堆棧上的每個線程都有自己獨立的堆棧所以局部變量不可能跨線程共享所以這樣的函數顯然是線程安全的
但值得注意的是下面的函數不是線程安全的
public static void Swap( ref int a ref int b )
//C++: void Swap( in& a int& b )
{
int c = a;
a = b;
b = c;
}
因為ref的存在使得函數的參數是按引用傳遞進來的換言之a和b看起來是函數的局部變量但實際上卻是函數外面的東西如果這兩個東西是另一個函數的局部變量倒也沒有問題如果這兩個東西是全局變量(靜態成員)就不能確保沒有線程沖突了而在上個例子中a和b在傳入函數之時就做了一個拷貝的動作所以傳進來的ab到底是全局變量還是靜態成員都沒有關系了
同樣這樣的函數也不是線程安全的
public static int Add( INumber a INumber b )
//C++: int Add( INumber* a INumber* b );
{
return aNumber + bNumber;
//C++: return a>Number + b>Number;
}
原因在於a和b雖然是函數的內部變量沒錯但aNumber和bNumber卻不是它們不存在於堆棧上而是在托管堆上可能被其他線程更改
但只使用局部變量的函數在NET類庫中是很少的但NET類庫中還是有那麼多線程安全的函數是為什麼呢?
因為即使一個函數使用了共享資源如果其所使用的共享資源都是線程安全的則這個函數也是線程安全的
比如說這樣的函數
private const string connectionString = …;
public string GetConnectionString()
{
return connectionString;
}
雖然這個函數使用了一個共享資源connectionString但因為這個資源是線程安全的所以這個函數還是線程安全的
同樣的我們可以得出如果一個函數只調用線程安全的函數只使用線程安全的共享資源
那麼這個函數也是線程安全的
這裡有一個容易被忽略的問題運算符並不是所有的運算符(尤其是重載後的運算符)都是線程安全的
四互斥鎖
有時候我們不得不面對線程不安全的問題比如說在一開始提出來的那個例子多線程完成次任務我們怎樣才能解決這個問題一個簡單的辦法就是給共享資源加上互斥鎖在C#中這很簡單比如一開始的那個例子
public static class Environment
{public static int count = ;//全局計數器
}
//…
void ThreadMethod()//運行在每個線程的方法
{
while( true )
{
lock ( typeof( Environment ) )
{
if ( count >= )//如果達到任務指標
break;//中斷線程執行
DoSomething();//完成某個任務
count++;}}}
通過互斥鎖使得一個線程在使用count字段的時候其他所有的線程都無法使用而被阻塞等待達到了避免線程沖突的效果
當然這樣的鎖會使得這個多線程程序退化成同時只有一個線程在跑所以我們可以把count++提前使得lock的范圍縮小如這樣
void ThreadMethod()//運行在每個線程的方法
{
while( true )
{
lock ( typeof( Environment ) )
{
if ( count++ >= )//如果達到任務指標
break;//中斷線程執行
}
DoSomething();//完成某個任務
}}
最後來聊聊SyncRoot的問題
用NET的一定會有很多朋友困惑為什麼對一個容器加鎖需要這樣寫
lock( ContainerSyncRoot )
而不是直接lock( Container )
因為鎖定一個容器並不能保證不會對這個容器進行修改考慮這樣一個容器
public class Collection
{
private ArrayList _list;
public Add( object item )
{
_listAdd( item );
}
public object this[ int index ]
{
get { return _list[index]; }
set { _list[index] = value;}
}}
看起來將其lock起來後就萬事大吉了沒有人能修改這個容器但實際上這個容器不過是用一個ArrayList實例來實現的如果某段代碼繞過這個容器而直接操作_list的話則對這個容器對象lock也不可能保證容器不被修改了
From:http://tw.wingwit.com/Article/program/net/201311/13071.html