併發技術中同步

_York發表於2018-06-14

  如果程式中用到了併發技術,一段程式碼需要修改資料,同時其他程式碼需要訪問同一資料。

  同步的型別: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);
        }

 

 

  

相關文章