[ C# 開發隨筆 ] 在 Async/Await 情況使用 ReaderWriterLockSlim 出現無法解鎖的狀況

async/await 滿天飛的.net core or .net 6 的專案,前陣子有人問到一個問題,她在鎖定同一時間只能一個人上傳檔案的時候,ReaderWriterLockSlim 無法解鎖。

在解鎖的時候會跳錯出錯誤[The write lock is being released without being held.] 這是什麼原因呢?請讓我們繼續看下去...

發生錯誤的程式碼

首先我們先上一段 Code ,這是一個 .net 6 的上傳檔案的API,做的事情都很單純,鎖定執行序然後寫入檔案,就回傳成功!

 private static ReaderWriterLockSlim _readerWriterLockSlim = new ReaderWriterLockSlim();
        

[HttpPost]
[Route("Upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
    try
    {
        // 鎖定
        if (!_readerWriterLockSlim.TryEnterWriteLock(50))
        {
            throw new Exception("Be Locked");
        }

        try
        {
            // 儲存上傳檔案
            var filePath = $"{Directory.GetCurrentDirectory()}/File/";
            if (!Directory.Exists(filePath))
            {
                Directory.CreateDirectory(filePath);
            }

            var path = $"{filePath}{file.Name}{DateTime.Now:yyyyMMddHHmmssfff}";
            await using (Stream stream = new FileStream(path, FileMode.Create))
            {
                // 重點問題在這行
                await file.CopyToAsync(stream);
            }

            return Ok("Success");
        }
        finally
        {
            // 會出錯的地方
            _readerWriterLockSlim.ExitWriteLock();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

這段程式執行後會收到一個 Exception : [The write lock is being released without being held.]  ,根據說明是這個鎖已經被解掉,但其實沒有解鎖,當你在上傳第二個檔案的時候,會得到被鎖定中的結果。

錯誤發生的原因:

會發生這件事情的主要原因是出在 [await file.CopyToAsync(stream);] 這行,你進來的執行序,執行到這邊的時候會把任務交給 IO Thread,原執行序會釋放掉,當 IO Thread完成的他的任務,會交由空著的執行序接手,通常不會是原本的那條執行序,因此我們可以得到一個結論,當 await 離開原執行序後回來就會換了一條新的執行序,更換執行序這件事情我們先稱之為「Thread-affine」。

但這會對我們造成什麼影響呢?在跟執行序無關的程式都不會有任何影響,只是執行序的ID改變,但 ReaderWriterLockSlim TryEnterWriteLock ExitWriteLock 是會根據執行序作判斷的,當你換了一條執行序回來之後,Exit 會判斷這條執行序沒有相應的 Lock,所以無法被釋放,但你原先執行序的鎖還在,於是導致沒有人可以進來的窘境。 

解決方式:

使用 AsyncReaderWriterLock 需安裝 Nuget 套件 Nito.AsyncEx ,程式碼如下:


private static AsyncReaderWriterLock _asyncReaderWriterLock = new AsyncReaderWriterLock();

[HttpPost]
[Route("Upload")]
public async Task<IActionResult> UploadFileV2(IFormFile file)
{

    try
    {
        using (var writerLockAsync = await _asyncReaderWriterLock.WriterLockAsync())
        {
            var filePath = $"{Directory.GetCurrentDirectory()}/File/";
            if (!Directory.Exists(filePath))
            {
                Directory.CreateDirectory(filePath);
            }

            Stream stream =
                new FileStream($"{filePath}{file.Name}{DateTime.Now:yyyyMMddHHmmssfff}",
                    FileMode.Create);
            var currentProcessorId = Thread.GetCurrentProcessorId();
            await file.CopyToAsync(stream);
            var currentProcessorIad = Thread.GetCurrentProcessorId();
            stream.Close();
        }

        return Ok("Success"); 
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

特別注意有很多執行序 Lock 都會遇到這個問題,使用Lock的時候還要多注意。

參考:

如有指正之處,歡迎隨時提出