[ 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的时候还要多注意。

参考:

https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-7-asyncreaderwriterlock/
https://github.com/StephenCleary/AsyncEx


<<:  四个可以帮助你找回 iPhone/iPad 上遗失的照片影片的实用方法

>>:  资安学习路上-网站常见漏洞与 Injection的爱恨情仇4

浅谈无状态这件事 Stateless

无状态stateless指的是web客户端在发送请求时,到底需不需要一直带着验证资讯,或者是所谓的上...

实体关联性模型图 ER/EER Diagram

ER Diagram (Entity Relationship Model) 是一个非常热门的资料库...

伸缩自如的Flask [day4] JWT

好的,你很辛苦的写了很多API function,但是你却不希望闲杂人等没事就call一下你的API...

[ Day 12 ] React 的生命周期 - Updating

今天要来进入到生命周期的第二个环节: Updating 更新,继上篇的 Mounting 元件挂载...

Day2 网路是一堆电缆构成的,那网页呢?

大致了解网路是什麽之後,那每天逛的网页又是什麽呢? 什麽是网页? 网页是一份档案,通常会储存在服务器...