在 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