標籤:
多線程編程對很多程式員來說並不容易,在啟動訪問相同資料的多個線程時,會間歇性地遇到難以發現的問題。如果使用任務、並行LINQ或Parallel類,也會遇到這些問題。為了避免這一系列問題,開發程式中必須注意同步問題和多個線程可能發生的其它問題。下面我們看一下競爭條件和死結。
一、競爭條件
如果兩個或多個線程訪問相同的對象,或者訪問不同步的共用狀態(例如EF的實體),就會出現競爭條件。為了說明競爭條件,我們定義一個StateObject類,它包含一個int欄位和一個ChangeState()方法。在該方法的實現代碼中,驗證狀態變數是否包含5,。如果包含,就遞增其值。然後用Trace.Assert方法來驗證state是否包含6.在給包含5的變數遞增了1後,可能希望變數的值是6,。但是事實卻不一定是這樣的。例如,如果一個線程剛剛執行完If(state==5)這一句代碼,它就被其它線程調用,調度器運行另一個線程。第二個線程剛進入if語句,因為state的值仍是5,所以將它遞增為6.現在,線程1再次被調度,那麼結果state就變成了7,這時就發生的競爭條件。
public class StateObject{ private int state=5; public void ChangeState(int loop){ if(state==5){ state++; Trace.Assert(state==6,"發生爭用"+loop+" loops"); } state=5; }}
下面通過給任務定義一個方法來驗證這一點,SameTask類的RaceCondition()方法經一個StateObject類作為參數。在一個無限while迴圈中,調用其方法。變數i僅用於標示迴圈次數。
public class SampleTask{ public void RaceCondition(object o){ Trace.Assert(o is StateObject,"o 必須是 StateObject類型"); StateObject state=o as StateObject; int i=0; while(true){ state.ChangeState(i++); } }}
下面我們在Main方法中,建立一個StateObject對象,它由所有任務共用。我們看一下代碼
static void Main(){ var state=new StateObject(); for(int i=0;i<20;i++){ Task.Factory.StartNew(new SampleTask().RaceCondition,state); } Thread.Sleep(10000);}
運行程式,我們會看到錯誤提示,多次啟動程式,會得到不同的結果,那麼我們如何避免類似的問題呢,我們可以鎖定共用的對象,這可以線上程中完成:用下面的lock語句鎖定線上程中共用的state變數。只有一個線程能在鎖定塊中處理共用的對象。由於這個對象在所有的線程之間共用,因此如果一個線程鎖定了改對象,那麼其他線程就必須等待改鎖的解除。一旦接受鎖定,線程就擁有該鎖定,直到改鎖定塊的你、末尾才解除鎖定。
public class SampleTask{ public void RaceCondition(object o){ Trace.Assert(o is StateObject,"o 必須是 StateObject類型"); StateObject state=o as StateObject; int i=0; while(true){ state.ChangeState(i++); lock(state) { state.ChangeState(i++); } } }}
在使用共用對象時,除了進行鎖定外,還可以將共用對象設定為安全執行緒的對象。其中ChangeState()方法包含了lock語句,由於不能鎖定state變數本身(只有參考型別才能用於鎖定),因此定義一個object類型的變數,將它用於lock語句。
public class StateObject{ private int state=5; private object o=new object(); public void ChangeState(int loop){ lock(o){ if(state==5){ state++; Trace.Assert(state==6,"發生爭用"+loop+" loops"); } state=5; } }}
二、死結
過多的鎖定也會有問題,在死結中,至少有兩個線程被掛起,並等待對象解除鎖定。由於兩個線程都在等待對方,就出現了死結,那麼後麵線程將無限期等待下去。
下面我們看一個死結的例子,我們建立兩個任務,
var state1=new StateObject();var state2=new StateObject();Task.Factory.StartNew(new SampleTask(state1,state2).Deadlock1);Task.Factory.StartNew(new SampleTask(state1,state2).Deadlock1);public class SampleThread{ private StateObject s1; private StateObject s2; public SampleThread(StateObject s1,StateObject s2) { this.s1=s1; this.s2=s2; } public void Deadlock1() { int i=0; while(true){ lock(s1){ lock(s2){ s1.ChangeState(i); s2.ChangeState(i++); Console.WriteLine("{0}",i); } } } } public void Deadlock2() { int i=0; while(true){ lock(s2){ lock(s1){ s1.ChangeState(i); s2.ChangeState(i++); Console.WriteLine("{0}",i); } } } }}
Deadlock1()和DeadLock2()方法現在改變兩個對象s1、s2的狀態,這容易造成死結,前一個方法先鎖定s1,接著鎖定s2,而兩一個則相反,現在有可能前者s1的鎖定被解除,出現一次線程切換,Deadlock2方法開始運行,並鎖定s2,那麼第二個線程現在等待s1鎖定的解鎖,因為它需要等待,所以線程調度器再次調度第一個線程,但第一個線程在等待s2的解鎖,那麼會造成兩個線程都在等待,只要鎖定塊沒有結束,就不會解鎖,結果就是造成了死結。
【C#】線程問題