C#基礎提高系列——C#任務同步

C#任務同步數組

若是須要共享數據,就必須使用同步技術,確保一次只有一個線程訪問和改變共享狀態。若是不注意同步,就會出現爭用條件和死鎖。安全

不一樣步致使的線程問題多線程

若是兩個或多個線程訪問相同的對象,而且對共享狀態的訪問沒有同步,就會出現爭用條件。爲了解決這類問題,可使用lock語句,對共享對象進行鎖定,除了進行鎖定以外,還能夠將共享對象設置爲線程安全的對象。異步

注意:只有引用類型才能使用lock進行鎖定。函數

鎖定並非越多越好,過多的鎖定會形成死鎖,在死鎖中,至少有兩個線程被掛起,並等待對象解除鎖定。因爲兩個線程都在等待對方,就出現了死鎖,線程將無限等待下去。優化

lock語句和線程安全this

C#爲多個線程的同步提供了本身的關鍵字:lock語句。spa

使用一個簡單的示例來講明lock的使用,首先定義兩個簡單的類來模擬線程計算,這兩個類不包含任何的鎖操做:操作系統

class SharedState { public int State { get; set; } } class Job { private SharedState _sharedState; public Job(SharedState sharedState) { this._sharedState = sharedState; } //該方法不是最終解決方案,存在漏洞,請不要直接應用到實際代碼中 public void DoTheJob() { for (int i = 0; i < 50000; i++) { //每循環一次,值+1 _sharedState.State += 1; } } }

接着使用並行任務同時調用上述方法,這裏使用循環建立了20個Task對象,代碼以下:.net

 public static void Run() { int numTasks = 20; //在循環外聲明一個SharedState實例,全部的Task都將接收該實例對象 var state = new SharedState(); //聲明Task數組 var tasks = new Task[numTasks]; for(int i = 0; i < numTasks; i++) { //傳入共用的SharedState實例 tasks[i] = Task.Run(() => new Job(state).DoTheJob()); } //等待全部任務的執行 Task.WaitAll(tasks); Console.WriteLine("結果:"+state.State); }

上述代碼沒有使用lock語句,多個Task對於_sharedState.State的訪問存在線程不安全的狀況,這就致使每次執行上述方法時輸出的結果各不相同而且仍是錯誤的(正確值是50000*20=100 0000)。屢次調用上述方法,輸出的結果以下:

結果:402798 結果:403463 結果:467736 結果:759837

爲了獲得正確結果,必須在這個程序中添加同步功能,可使用lock關鍵字實現,它表示要等待指定對象的鎖定。當鎖定了一個線程後,就能夠運行lock語句塊。在lock語句塊結束時,對象的鎖定被解除,另外一個等待鎖定的線程就能夠得到該鎖定塊了。lock語句只能傳遞引用類型,由於值類型只是鎖定了一個副本,並無任何意義。

使用lock語句,若是要鎖定靜態成員,能夠把鎖放在object類型或靜態成員上;若是要將類的實例成員設置爲線程安全的(一次只能有一個線程訪問相同實例的成員),能夠在類中單獨定義一個object類型的成員對象,在該類的其餘成員只用將這個對象用於lock語句。

Job類中,對DoTheJob()方法進行改寫,使用lock語句進行鎖定,方法以下:

 public void DoTheJob() { for (int i = 0; i < 50000; i++) { lock (_sharedState) { _sharedState.State += 1; } } }

接着執行以前的Run()方法,此時能夠獲得正確的值:

結果:1000000 -----程序執行完畢-----

Interlocked類

對於經常使用的i++這種運算,在多線程中,它並非線程安全的,它的操做包括從內存中獲取一個值,給該值遞增1,再將它存儲回內存中。這些操做均可能被線程調度器打斷。Interlocked類提供了以線程安全的方式遞增、遞減、交換和讀取值的方法。

在使用lock語句對相似i++這種操做進行鎖同步時,使用Interlocked類會快的多。可是,它只能用於簡單的同步問題。

示例一,使用lock語句鎖定對某個變量的訪問,對該變量進行比較操做:

lock (obj) { if (someState == null) { someState = newState; } }

上述可使用Interlocked.CompareExchange()方法進行改寫,而且執行的更快:

Interlocked.CompareExchange(ref someState, newState, null);

示例二,若是是簡單的對變量遞增進行lock語句:

lock (obj) { return ++_state; }

可使用執行更快的Interlocked.Increment()方法進行改寫:

Interlocked.Increment(ref _state);

Monitor類

lock語句由C#編譯器解析爲使用Monitor類。

lock(obj) { }

上述lock語句被解析爲調用Monitor類的Enter()方法,該方法會一直等待,直到線程鎖定對象爲止。一次只有一個線程能鎖定對象。只要解除了鎖定,線程就能夠進入同步階段【只要對象被鎖定,線程就能夠進入同步階段】。Monitor類的Exit()方法解除了鎖定。編譯器把Exit()方法放在try塊的finally處理程序中,因此若是拋出了異常,就會解除該鎖定。

Monitor.Enter(obj); try { //同步執行代碼塊 } finally { Monitor.Exit(obj); }

與C#的lock語句相比,Monitor類的主要優勢是:能夠添加一個等待被鎖定的超時值。這樣其餘線程就不會無限期地等待被鎖定。可使用Monitor.TryEnter()方法,併爲該方法傳遞一個超時值,指定等待被鎖定的最長時間。

bool _lockTaken = false; Monitor.TryEnter(_obj, 500, ref _lockTaken); if (_lockTaken) { try { } finally { Monitor.Exit(_obj); } } else { //didn't get the lock,do something else }

上述中,若是obj被鎖定,TryEnter()方法就把布爾型的引用參數設置爲true,並同步的訪問由對象obj鎖定的狀態。若是另個一線程鎖定obj的時間超過了500毫秒,TryEnter()方法就把變量lockTaken設置爲false,線程不在等待,而是用於執行其餘操做。也許在之後,該線程會嘗試再次得到鎖定。

SpinLock結構

SpinLock結構的用法很是相似於Monitor類。使用Enter()TryEnter()方法得到鎖,使用Exit()方法釋放鎖定。與Monitor相比,若是基於對象的鎖定對象(使用Monitor)的系統開銷因爲垃圾回收而太高,就可使用SpinLock結構。若是有大量的鎖定,且鎖定的時間老是很是短,SpinLock結構就頗有用。應避免使用多個SpinLock結構,也不要調用任何可能阻塞的內容。

SpinLock結構還提供了屬性IsHeldIsHeldByCurrentThread,指定它當前是否被鎖定。

注意:因爲SpinLock定義爲結構,所以傳遞SpinLock實例時,是按照值類型傳遞的。

WaitHandle抽象類

WaitHandle是一個抽象基類,用於等待一個信號的設置。能夠等待不一樣的信號,由於WaitHandle是一個基類,能夠從中派生一些其餘類。

異步委託的BeginInvoke()方法返回一個實現了IAsycResult接口的對象。使用IAsycResult接口,能夠用AsycWaitHandle屬性訪問WaitHandle基類。在調用WaitHandleWaitOne()方式或者超時發生是,線程會等待接收一個與等待句柄相關的信號。調用EndInvoke()方法,線程最終會阻塞,知道獲得結果爲止。

示例以下:

static int TakesAWhile(int x,int ms) { Task.Delay(ms).Wait(); return 42; } delegate int TakesAWhileDelegate(int x, int ms); public static void Run() { TakesAWhileDelegate d1 = TakesAWhile; IAsyncResult ar= d1.BeginInvoke(1, 3000, null, null); while (true) { if (ar.AsyncWaitHandle.WaitOne(50)) { Console.WriteLine("Can get the result now"); break; } } int result = d1.EndInvoke(ar); Console.WriteLine("result:"+result); }

調用上述方法,輸出結果以下:

Can get the result now result:42 -----程序執行完畢-----

使用WaitHandle基類能夠等待一個信號的出現(WaitOne()方法)、等待必須發出信號的多個對象(WaitAll()方法),或者等待多個對象中的一個(WaitAny()方法)。WaitAll()WaitAny()WaitHandle類的靜態方法,接收一個WaitHandle參數數組。

WaitHandle基類有一個SafeWaitHandle屬性,其中能夠將一個本機句柄賦予一個操做系統資源,並等待該句柄。例如,能夠指定一個SafeFileHandle等待文件I/O操做的完成。

由於MutexEventWaitHandleSemaphore類派生自WaitHandle基類,因此能夠在等待時使用它們。

Mutex類

Mutex(mutual exclusion,互斥)是.NET Framework中提供跨多個進程同步訪問的一類。它很是相似於Monitor類,由於它們都只有一個線程能擁有鎖定。只有一個線程能得到互斥鎖定,訪問受互斥保護的同步代碼區域。

Mutex類的構造函數中,能夠指定互斥是否最初應由主調線程擁有,定義互斥的名稱,得到互斥是否已存在的信息。

bool createdNew; var mutex=new Mutex(false,"ProCSharpMutex",out createdNew);

上述示例代碼中,第3個參數定義爲輸出參數,接收一個表示互斥是否爲新建的布爾值。若是返回值爲false,就表示互斥已經定義。互斥能夠在另外一個進程中定義,由於操做系統可以識別有名稱的互斥,它由不一樣的進程共享。若是沒有給互斥指定名稱,互斥就是爲命名的,不在不一樣的進程之間共享。

因爲系統能識別有名稱的互斥,所以可使用它禁止應用程序啓動兩次,經常使用於WPF/winform中:

bool mutexCreated; var mutex=new Mutex(false,"SingleOnWinAppMutex",out mutexCreated); if(!mutexCreated){ MessageBox.Show("當前程序已經啓動!"); Application.Current.Shutdown(); }

Semaphore類

Semaphore很是相似於Mutex,其區別是,Semaphore能夠同時由多個線程使用,它是一種計數的互斥鎖定。使用Semaphore,能夠定義容許同時訪問受鎖定保護的資源的線程個數。若是須要限制能夠訪問可用資源的線程數,Semaphore就頗有用。

.NET Core中提供了兩個類SemaphoreSemaphoreSlimSemaphore類可使用系統範圍內的資源,容許在不一樣進程之間同步。SemaphoreSlim類是對較短等待時間進行了優化的輕型版本。

static void TaskMain(SemaphoreSlim semaphore) { bool isCompleted = false; while (!isCompleted) { //鎖定信號量,定義最長等待時間爲600毫秒 if (semaphore.Wait(600)) { try { Console.WriteLine($"Task {Task.CurrentId} locks the semaphore"); Task.Delay(2000).Wait(); } finally { Console.WriteLine($"Task {Task.CurrentId} releases the semaphore"); semaphore.Release(); isCompleted = true; } } else{ Console.WriteLine($"Timeout for task {Task.CurrentId}; wait again"); } } } public static void Run() { int taskCount = 6; int semaphoreCount = 3; //建立計數爲3的信號量 //該構造函數第一個參數表示最初釋放的鎖定量,第二個參數定義了鎖定個數的計數 var semaphore = new SemaphoreSlim(semaphoreCount, semaphoreCount); var tasks = new Task[taskCount]; for(int i = 0; i < taskCount; i++) { tasks[i] = Task.Run(()=>TaskMain(semaphore)); } Task.WaitAll(tasks); Console.WriteLine("All tasks finished"); }

上述代碼中的Run()方法中,建立了6個任務和一個計數爲3的信號量。在SemaphoreSlim類的構造方法中,第一個參數定義了最初釋放的鎖定數,第二個參數定義了鎖定個數的計數。若是第一個參數的值小於第二個參數,它們的差就是已經分配線程的計數值。與互斥同樣,能夠給信號量指定名稱,使之在不一樣的進程之間共享。實例中,定義信號量時沒有指定名稱,因此它只能在這個進程中使用。

上述代碼中的TaskMain()方法中,任務利用Wait()方法鎖定信號量。信號量的計數是3,因此有3個任務能夠得到鎖定。第4個任務必須等待,這裏還定義了最長等待時間爲600毫秒。若是在該等待時間事後未能得到鎖定,任務就把一條消息寫入控制檯,在循環中繼續等待。只要得到了鎖定,任務就把一條消息寫入控制檯,等待一段時間,而後解除鎖定。在解除鎖定時,在任何狀況下必定要解除資源的鎖定,這一點很重要。這就是要在finally處理程序中調用SemaphoreSlim.Release()方法的緣由。

上述代碼執行後,輸出結果以下:

Task 3 locks the semaphore Task 2 locks the semaphore Task 1 locks the semaphore Timeout for task 4; wait again Timeout for task 4; wait again Timeout for task 5; wait again Timeout for task 4; wait again Task 1 releases the semaphore Task 9 locks the semaphore Task 3 releases the semaphore Task 5 locks the semaphore Task 2 releases the semaphore Task 4 locks the semaphore Task 4 releases the semaphore Task 5 releases the semaphore Task 9 releases the semaphore All tasks finished -----程序執行完畢-----

Events類(略)

此處的Events並非C#中的某個類名,而是一系列類的統稱。主要使用到的類有ManualResetEventAutoResetEventManualResetEventSlimCountdownEvent類。與MutexSemaphore對象同樣,Events對象也是一個系統範圍內的資源同步方法。

注意:C#中的event關鍵字與System.Threading命名空間中的event類沒有任何關係。event關鍵字基於委託,而上述event類是.net封裝器,用於系統範圍內的本機事件資源的同步。

可使用Events通知其餘任務:這裏有一些數據,並完成了一些操做等。Events能夠發信號,也能夠不發信號。

Barrier類(略)

對於同步,Barrier類很是適用於其中工做有多個任務分支且之後又須要合併工做的狀況。Barrier類用於須要同步的參與者。激活一個任務時,就能夠動態的添加其餘參與者。

Barrier類型提供了一個更復雜的場景,其中能夠同時運行多個任務,直到達到一個同步點爲止。一旦全部任務達到這一點,他們舊客戶以繼續同時知足於下一個同步點。

ReaderWriterLockSlim類(略)

爲了使鎖定機制容許鎖定多個讀取器(而不是一個寫入器)訪問某個資源,可使用ReaderWriterLockSlim類。這個類提供了一個鎖定功能,若是沒有寫入器鎖定資源,就容許多個讀取器訪問資源,但只能有一個寫入器鎖定該資源。

Timer類(略)

使用計時器,能夠重複調用方法。

任務同步補充說明

上述內容帶略的都是不多使用到的,可是不表明必定不會用到。建議實際應用中經過官方文檔去了解具體的用法。

在使用多個線程時,儘可能避免共享狀態,若是實在不可避免要用到同步,儘可能使同步要求最低化,由於同步會阻塞線程。

相關文章
相關標籤/搜索