詳解.NET編程過程中的線程衝突

一、什麼是線程衝突

線程衝突其實就是指,兩個或以上的線程同時對同一個共享資源進行操作而造成的問題。


一個比較經典的例子是,用一個全局變量做計數器,然後開N個線程去完成某個任務,每個線程完成一次任務就將計數器加一,直到完成100次任務。如果不考慮線
程衝突問題,用類似下面的代碼去做,則很可能會超額完成任務,線程越多,完成任務次數超出100次的可能性就越大。

僞代碼如
下:

int count = 0; //全局計數器

void
ThreadMethod()//運行在每個線程的方法

{

while( true )

{

if( count >= 100 )//如果達到任務指標

break; //中斷線程執行

DoSomething();
//完成某個任務

count++;

}

}

//省略線程的創建等代碼。


具體的,爲什麼會超額完成任務的原因在這裏我就不贅述了,這個例子在單線程環境中是絕對不會超額完成任務的。


當然,在這個例子中,將count++放到if語句中,也許能降低一些事故發生的概率,但那不是絕對的,換言之這樣的程序不能杜絕超額完成任務的可能。

其實從線程衝突的定義中我們不難發現,要造成線程衝突有兩個必要條件:多線程和共享資源。這兩個條件中有一個不成立,就不可能發生線程衝突問題。

所以,在單線程環境中,是不存在線程衝突的問題的。不過很可惜的是,我們的軟件早已進化到了多進程多線程的時代,單線程的程序幾乎是不存在的,無論是
WinForm還是WebForm,程序運行的環境都是多線程的,而不論你自己是不是明確的開啓了一個線程。

既然多線程是不可避免的,那麼要避免線程衝突就只能從共享資源來開刀了。

二、線程安全的資源

如果大家經常看MSDN
或者VS幫助中的.NET類庫參考的話,就不難發現幾乎所有的類型都有這麼一句話的描述:“此類型的任何公共 static(在 Visual
Basic中爲 Shared) 成員都是線程安全的。但不保證所有實例成員都是線程安全的。”那麼線程安全到底是什麼意思?

其實線程安全很簡單,就是指一個函數(方法、屬性、字段或者別的)在同一時間被不同線程使用,不會造成任何線程衝突的問題。就說這個東西是線程安全的。

接下來來談談什麼樣的資源是線程安全的。

之所以使用資源這個詞,是因爲線程衝突不僅僅會發生在共享的變量上,兩個線程同時對同一
個文件進行讀寫,兩個程序同時用同一個端口與同一個地址進行通信,都會造成線程衝突。只不過是操作系統和幫我們協調了這些衝突而已。

一個線程安全的資源即是指,在不同線程中使用不會導致線程衝突問題的資源。

一個不能被改變的資源是線程安全的,比如說一個常量:

const decimal pai = 3.14159265; //C++: const double pai = 3.14159265;

因爲pai的值不可能被改變,所以在不同的線程中使用也不會造成衝突。換言之它在不同的線程中同時被使用和在一個線程中被使用是沒有區別的,所以這個東西是線程安全的。

同樣的,在.NET中,一個字符串的實例也是線程安全的,因爲字符串的實例在.NET中也是不可以被改變的。一個字符串的實例一旦被創建,對其所有的屬性、方法調用的結果都是唯一確定的,永遠不會改變的。所以.NET類庫參考中String類型纔有:“此類型是線程安全的。”,與之類似的Type類型、Assembly類型,都是線程安全的。


但string的實例是線程安全的,卻不代表string的變量是線程安全的,換言之,假設有一個靜態變量:

public static string str = “123”;

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在傳入函數之時,
就做了一個拷貝的動作,所以傳進來的a、b到底是全局變量還是靜態成員都沒有關係了。
同樣,這樣的函數也不是線程安全的:
public static int Add( INumber a,
INumber b )//C++: int Add( INumber* a, INumber* b );
{
return
a.Number + b.Number;
//C++: return a->Number +
b->Number;
}


原因在於a和b雖然是函數的內部變量沒錯,但a.Number和
b.Number卻不是,它們不存在於堆棧上,而是在託管堆上,可能被其他線程更改。

但只使用局部變量的函數在.NET類庫中是很少的,但.NET類庫中還是有那麼多線程安全的函數,是爲什麼呢?

因爲,即使一個函數使用了共享資源,如果其所使用的共享資源都是線程安全的,則這個函數也是線程安全的。

比如說這樣的函數:
private const string connectionString = “…”; public string GetConnectionString()
{
return connectionString;
}

雖然這個函數使用了一個共享資源connectionString,但因爲這個資源是線程安全的,所以這個函數還是線程安全的。
同樣的,我們可以得出,如果一個函數只調用線程安全的函數,只使用線程安全的共享資源,那麼這個函數也是線程安全的。

這裏有一個容易被忽略的問題,運算符。並不是所有的運算符(尤其是重載後的運算符)都是線程安全的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章