如果程式中用到了併發技術,一段程式碼需要修改資料,同時其他程式碼需要訪問同一資料。
同步的型別:a.通訊 b.資料保護。
如果以下三個條件都滿足,就需要使用同步來保護資料。
- 多段程式碼正在併發執行;
- 這幾段程式碼在訪問(讀或寫)同一個資料;
- 至少有一段程式碼在修改資料。
1、阻塞鎖 lock
多個執行緒需要安全的讀寫共享資料。
一個執行緒進入鎖後,在鎖被釋放之前,其他執行緒是無法進入的。
鎖的使用,有四條重要的規則:
- 限制鎖的作用範圍
- 文件中寫清鎖保護的內容
- 鎖範圍內的程式碼儘量少
- 在控制鎖的時候,絕不執行隨意的程式碼
首先,要儘量限制鎖的作用範圍。應該把 lock 語句使用的物件設為私有成員,並且永遠不
要暴露給非本類的方法。每個型別通常最多隻有一個鎖。如果一個型別有多個鎖,可考慮通
過重構把它分拆成多個獨立的型別。可以鎖定任何引用型別,但是我建議為 lock 語句定義
一個專用的成員,就像最後的例子那樣。尤其是千萬不要用 lock(this),也不要鎖定 Type
或 string 型別的例項。因為這些物件是可以被其他程式碼訪問的,這樣鎖定會產生死鎖。
第二,要在文件中描述鎖定的內容。這種做法在最初編寫程式碼時很容易被忽略,但是在代
碼變得複雜後就會變得很重要。
第三,在鎖定時執行的程式碼要儘可能得少。要特別小心阻塞呼叫。在鎖定時不要做任何阻
塞操作。
最後,在鎖定時絕不要呼叫隨意的程式碼。隨意的程式碼包括引發事件、呼叫虛擬方法、呼叫
委託。如果一定要執行隨意的程式碼,就在釋放鎖之後執行
2、非同步鎖 SemaphoreSlim
多個程式碼需要安全讀寫資料,並且這些程式碼塊可能使用await語句。同步鎖的規則同樣適用於非同步鎖。
public class MyClass { /// <summary> /// 次鎖保護_value /// </summary> private readonly SemaphoreSlim _mutx = new SemaphoreSlim(1); private int _value; public async Task DelayAndIncrementAsync() { await _mutx.WaitAsync(); try { await Task.Delay(TimeSpan.FromSeconds(_value)); _value = _value + 1; } finally { _mutx.Release(); } } }
3、阻塞訊號 ManualResetEventSlim
需要從一個執行緒傳送訊號給另外一個執行緒
public class MyClass { private readonly ManualResetEventSlim _resetEvent = new ManualResetEventSlim(); private int _value; private int WaitForInitialization() { _resetEvent.Wait(); return _value; } private void InitializeFromAnotherThread() { _value = 10; _resetEvent.Set(); } }
ManualResetEventSlim 是功能強大、通用的執行緒間訊號,但必須合理地使用
4、非同步訊號
需要在程式碼的各個部分間傳送通知,並且要求接收方必須進行非同步等待。
public class MyClass { private readonly TaskCompletionSource<object> _initialized = new TaskCompletionSource<object>(); private int _value1; private int _value2; public async Task<int> WaitForInitializationAsync() { await _initialized.Task; return _value1 + -_value2; } public void Initialize() { _value1 = 10; _value2 = 5; _initialized.TrySetResult(null); } }
在所有情況下都可以用 TaskCompletionSource<T> 來非同步地等待:本例中,通知來自於另一
部分程式碼。如果只需要傳送一次訊號,這種方法很適合。但是如果要開啟和關閉訊號,這
種方法就不大合適了.。
5、限流
有一段高度併發的程式碼,由於它的併發程度實在太高了,需要有方法對併發性進行限流。可以避免資料項佔用太多的記憶體。
如果發現程式的CPU或者網路連線數太多了,或者記憶體佔用太多,就需要進行限流。
資料流和並行程式碼都自帶了對併發性限流的方法:
IEnumerable<int> ParallelMultiplyBy2(IEnumerable<int> values) { return values.AsParallel() .WithDegreeOfParallelism(10) .Select(item => item * 2); }
併發性非同步程式碼可以使用 SemaphoreSlim 來限流
async Task<string[]> DownloadUrlsAsync(IEnumerable<string> urls) { var httpClient = new HttpClient(); var semaphore = new SemaphoreSlim(10); var tasks = urls.Select(async url => { await semaphore.WaitAsync(); try { return await httpClient.GetStringAsync(url); } finally { semaphore.Release(); } }).ToArray(); return await Task.WhenAll(tasks); }