詳解.NET編程過(guò)程中的線程沖突
一、什么是線程沖突
線程沖突其實(shí)就是指,兩個(gè)或以上的線程同時(shí)對(duì)同一個(gè)共享資源進(jìn)行操作而造成的問(wèn)題。
一個(gè)比較經(jīng)典的例子是,用一個(gè)全局變量做計(jì)數(shù)器,然后開(kāi)N個(gè)線程去完成某個(gè)任務(wù),每個(gè)線程完成一次任務(wù)就將計(jì)數(shù)器加一,直到完成100次任務(wù)。如果不考慮線程沖突問(wèn)題,用類似下面的代碼去做,則很可能會(huì)超額完成任務(wù),線程越多,完成任務(wù)次數(shù)超出100次的可能性就越大。
偽代碼如下:
|
具體的,為什么會(huì)超額完成任務(wù)的原因在這里我就不贅述了,這個(gè)例子在單線程環(huán)境中是絕對(duì)不會(huì)超額完成任務(wù)的。
當(dāng)然,在這個(gè)例子中,將count++放到if語(yǔ)句中,也許能降低一些事故發(fā)生的概率,但那不是絕對(duì)的,換言之這樣的程序不能杜絕超額完成任務(wù)的可能。
其實(shí)從線程沖突的定義中我們不難發(fā)現(xiàn),要造成線程沖突有兩個(gè)必要條件:多線程和共享資源。這兩個(gè)條件中有一個(gè)不成立,就不可能發(fā)生線程沖突問(wèn)題。
所以,在單線程環(huán)境中,是不存在線程沖突的問(wèn)題的。不過(guò)很可惜的是,我們的軟件早已進(jìn)化到了多進(jìn)程多線程的時(shí)代,單線程的程序幾乎是不存在的,無(wú)論是WinForm還是WebForm,程序運(yùn)行的環(huán)境都是多線程的,而不論你自己是不是明確的開(kāi)啟了一個(gè)線程。
既然多線程是不可避免的,那么要避免線程沖突就只能從共享資源來(lái)開(kāi)刀了。
二、線程安全的資源
如果大家經(jīng)??碝SDN或者VS幫助中的.NET類庫(kù)參考的話,就不難發(fā)現(xiàn)幾乎所有的類型都有這么一句話的描述:“此類型的任何公共 static(在 Visual Basic中為 Shared) 成員都是線程安全的。但不保證所有實(shí)例成員都是線程安全的?!蹦敲淳€程安全到底是什么意思?
其實(shí)線程安全很簡(jiǎn)單,就是指一個(gè)函數(shù)(方法、屬性、字段或者別的)在同一時(shí)間被不同線程使用,不會(huì)造成任何線程沖突的問(wèn)題。就說(shuō)這個(gè)東西是線程安全的。
接下來(lái)來(lái)談?wù)勈裁礃拥馁Y源是線程安全的。
之所以使用資源這個(gè)詞,是因?yàn)榫€程沖突不僅僅會(huì)發(fā)生在共享的變量上,兩個(gè)線程同時(shí)對(duì)同一個(gè)文件進(jìn)行讀寫(xiě),兩個(gè)程序同時(shí)用同一個(gè)端口與同一個(gè)地址進(jìn)行通信,都會(huì)造成線程沖突。只不過(guò)是操作系統(tǒng)和幫我們協(xié)調(diào)了這些沖突而已。
一個(gè)線程安全的資源即是指,在不同線程中使用不會(huì)導(dǎo)致線程沖突問(wèn)題的資源。
一個(gè)不能被改變的資源是線程安全的,比如說(shuō)一個(gè)常量:
const decimal pai = 3.14159265; |
因?yàn)閜ai的值不可能被改變,所以在不同的線程中使用也不會(huì)造成沖突。換言之它在不同的線程中同時(shí)被使用和在一個(gè)線程中被使用是沒(méi)有區(qū)別的,所以這個(gè)東西是線程安全的。
同樣的,在.NET中,一個(gè)字符串的實(shí)例也是線程安全的,因?yàn)樽址膶?shí)例在.NET中也是不可以被改變的。一個(gè)字符串的實(shí)例一旦被創(chuàng)建,對(duì)其所有的屬性、方法調(diào)用的結(jié)果都是唯一確定的,永遠(yuǎn)不會(huì)改變的。所以.NET類庫(kù)參考中String類型才有:“此類型是線程安全的。”,與之類似的Type類型、Assembly類型,都是線程安全的。
但string的實(shí)例是線程安全的,卻不代表string的變量是線程安全的,換言之,假設(shè)有一個(gè)靜態(tài)變量:
public static string str = “123”; |
str不是線程安全的,因?yàn)閟tr這個(gè)變量的字符串實(shí)例可以被任何線程修改。
再考慮這樣的例子:
public static readonly SqlConnection connection = new SqlConnection( “connectionString” );
雖然connection本身雖然是線程安全的,但connection的任何成員都不是線程安全的。
比如說(shuō),我在一個(gè)線程中對(duì)這個(gè)connection調(diào)用了Open方法,然后進(jìn)行查詢操作。但在同一時(shí)刻,另一個(gè)線程調(diào)用了Close方法,這時(shí)候,就出現(xiàn)錯(cuò)誤了。
但,單純的使用connection而不使用其任何成員,比如說(shuō)if ( connection != null )這樣的代碼,是不存在線程沖突的。
線程安全的資源其實(shí)還有很多,在此不一一贅述。
對(duì)于.NET Framework的類型的成員來(lái)說(shuō),只讀的字段是線程安全的。
那么對(duì)于屬性和方法來(lái)說(shuō),怎么知道是不是線程安全的?
三、線程安全的函數(shù)
因?yàn)閷傩院头椒ǘ际呛瘮?shù)組成的,所以我們探討一下什么是線程安全的函數(shù)。
上面我們說(shuō)到,線程沖突的必要條件是多線程和共享資源。那么如果一個(gè)函數(shù)里面沒(méi)有使用任何可能共享的資源,那么就不可能出現(xiàn)線程沖突,也就是線程安全的。比如說(shuō)這樣的函數(shù):
public static int Add( int a, int b ) |
這個(gè)函數(shù)中所使用的所有的資源都是自己的局部變量,而函數(shù)的局部變量是儲(chǔ)存在堆棧上的,每個(gè)線程都有自己獨(dú)立的堆棧,所以局部變量不可能跨線程共享。所以這樣的函數(shù)顯然是線程安全的。
但值得注意的是:下面的函數(shù)不是線程安全的:
public static void Swap( ref int a, ref int b ) |
因?yàn)閞ef的存在,使得函數(shù)的參數(shù)是按引用傳遞進(jìn)來(lái)的,換言之a(chǎn)和b看起來(lái)是函數(shù)的局部變量,但實(shí)際上卻是函數(shù)外面的東西,如果這兩個(gè)東西是另一個(gè)函數(shù)的局部變量,倒也沒(méi)有問(wèn)題,如果這兩個(gè)東西是全局變量(靜態(tài)成員),就不能確保沒(méi)有線程沖突了。而在上個(gè)例子中,a和b在傳入函數(shù)之時(shí),就做了一個(gè)拷貝的動(dòng)作,所以傳進(jìn)來(lái)的a、b到底是全局變量還是靜態(tài)成員都沒(méi)有關(guān)系了。
同樣,這樣的函數(shù)也不是線程安全的:
public static int Add( INumber a, INumber b ) |
原因在于a和b雖然是函數(shù)的內(nèi)部變量沒(méi)錯(cuò),但a.Number和b.Number卻不是,它們不存在于堆棧上,而是在托管堆上,可能被其他線程更改。
但只使用局部變量的函數(shù)在.NET類庫(kù)中是很少的,但.NET類庫(kù)中還是有那么多線程安全的函數(shù),是為什么呢?
因?yàn)椋词挂粋€(gè)函數(shù)使用了共享資源,如果其所使用的共享資源都是線程安全的,則這個(gè)函數(shù)也是線程安全的。
比如說(shuō)這樣的函數(shù):
private const string connectionString = “…”; |
雖然這個(gè)函數(shù)使用了一個(gè)共享資源connectionString,但因?yàn)檫@個(gè)資源是線程安全的,所以這個(gè)函數(shù)還是線程安全的。
同樣的,我們可以得出,如果一個(gè)函數(shù)只調(diào)用線程安全的函數(shù),只使用線程安全的共享資源,那么這個(gè)函數(shù)也是線程安全的。
這里有一個(gè)容易被忽略的問(wèn)題,運(yùn)算符。并不是所有的運(yùn)算符(尤其是重載后的運(yùn)算符)都是線程安全的。
四、互斥鎖
有時(shí)候我們不得不面對(duì)線程不安全的問(wèn)題,比如說(shuō)在一開(kāi)始提出來(lái)的那個(gè)例子,多線程完成100次任務(wù),我們?cè)鯓硬拍芙鉀Q這個(gè)問(wèn)題,一個(gè)簡(jiǎn)單的辦法就是給共享資源加上互斥鎖。在C#中這很簡(jiǎn)單。比如一開(kāi)始的那個(gè)例子:
public static class Environment |
通過(guò)互斥鎖,使得一個(gè)線程在使用count字段的時(shí)候,其他所有的線程都無(wú)法使用,而被阻塞等待。達(dá)到了避免線程沖突的效果。
當(dāng)然,這樣的鎖會(huì)使得這個(gè)多線程程序退化成同時(shí)只有一個(gè)線程在跑,所以我們可以把count++提前,使得lock的范圍縮小,如這樣:
void ThreadMethod()//運(yùn)行在每個(gè)線程的方法 |
最后來(lái)聊聊SyncRoot的問(wèn)題。
用.NET的一定會(huì)有很多朋友困惑,為什么對(duì)一個(gè)容器加鎖,需要這樣寫(xiě):
lock( Container.SyncRoot )
而不是直接lock( Container )
因?yàn)殒i定一個(gè)容器并不能保證不會(huì)對(duì)這個(gè)容器進(jìn)行修改,考慮這樣一個(gè)容器:
public class Collection |
看起來(lái),將其lock起來(lái)后,就萬(wàn)事大吉了,沒(méi)有人能修改這個(gè)容器,但實(shí)際上這個(gè)容器不過(guò)是用一個(gè)ArrayList實(shí)例來(lái)實(shí)現(xiàn)的,如果某段代碼繞過(guò)這個(gè)容器而直接操作_list的話,則對(duì)這個(gè)容器對(duì)象lock也不可能保證容器不被修改了。
【編輯推薦】