[料理佳餚] 用 SemaphoreSlim 來做 async/await 的鎖定

  • 8889
  • 0
  • C#
  • 2022-01-28

在 C# 應用程式內部要做鎖定,第一時間我們一定是先想到 lock 陳述式,但是 lock 陳述式無法在 async/await 的場景下使用,程式編譯不會通過,我們會得到一個錯誤訊息 - 無法在 lock 陳述式的主體中等候

即使我們使用 Monitor.Enter()Monitor.Exit() 騙過編譯器,最終也會在 Runtime 的時候得到一個 Exception:

System.Threading.SynchronizationLockException: 'Object synchronization method was called from an unsynchronized block of code.'

原因都是因為 await 之後的程式碼,不保證會在同一個 Thread 執行,這導致鎖被破壞了,因此在這種非同步的環境底下要使用鎖定,除了尋求外部的資源進行鎖定之外,在 C# 中還有一個 SemaphoreSlim 類別可以協助我們來完成跨執行緒的鎖定。

SemaphoreSlim 的運作概念

SemaphoreSlim 從 .NET Framework 4.0 開始出現,是 Semaphore 的輕量化版本,使用起來更為簡便,它的運作概念是這樣的,可以想像有一個工具箱,一開始被放置了 n 把工具,整個工具箱最多可以放置 m 把工具,每個工人需要在工作之前從工具箱取走至少 1 把工具才能進行作業,直到工具箱沒有工具時,之後的工人就必須等待,等到有工人完成作業將工具歸還到工具箱,才能繼續從工具箱取走工具進行作業,而總工具數量也不是一成不變的,可以視整體的工作情況進行增減,但要注意工具箱的可容納上限,整個概念對照 SemaphoreSlim 實際的屬性跟方法如下:

  • 一個工具箱:一個 SemaphoreSlim 實例
  • 一開始被放置的 n 把工具:SemaphoreSlim 建構式的 initialCount
  • 最多可以放置 m 把工具:SemaphoreSlim 建構式的 maxCount,預設值為 Int32.MaxValue。
  • 工人:執行程式的 Thread
  • 取走工具:SemaphoreSlim 的 Wait() 方法
  • 歸還工具:SemaphoreSlim 的 Release() 方法
  • 增減總工具數量:Wait() 少、Release() 多;Wait() 多、Release() 少。

與 async/await 結合

SemaphoreSlim 能用在 async/await 上最主要的是,它並沒有強制必須由同一個 Thread 來 Wait() 和 Release(),這讓我們在非同步的情境下也能夠使用鎖定,下面我用一個範例來說明,有一個 sum 扮演被競爭的資源,有一個非同步方法 DoAdding() 每執行一次就對 sum 加 1,如果我們什麼都不做,一次執行個 10,000 次,sum 的結果大概率不會如預期的每次都是 10,000。

為此我們可以使用 SemaphoreSlim 來限制同時執行 DoAdding() 方法的數量,確保每次都只有一個 DoAdding() 方法可以對 sum 加 1,藉此來保護競爭的資源。

參考資料

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學