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


C#任務同步

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

不同步導致的線程問題

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

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

鎖定並不是越多越好,過多的鎖定會造成死鎖,在死鎖中,至少有兩個線程被掛起,並等待對象解除鎖定。由於兩個線程都在等待對方,就出現了死鎖,線程將無限等待下去。

lock語句和線程安全

C#為多個線程的同步提供了自己的關鍵字:lock語句。

使用一個簡單的示例來說明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對象,代碼如下:

 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類(略)

使用計時器,可以重復調用方法。

任務同步補充說明

上述內容帶略的都是很少使用到的,但是不代表一定不會用到。建議實際應用中通過官方文檔去了解具體的用法。

在使用多個線程時,盡量避免共享狀態,如果實在不可避免要用到同步,盡量使同步要求最低化,因為同步會阻塞線程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM