关于C#中ReadWriterLockSlim解锁及给定异步代码的技术问询
嘿,我来帮你梳理这两个问题——先说说ReadWriterLockSlim的正确解锁姿势,再分析你给出的Refresh代码片段。
ReadWriterLockSlim是解决读写并发问题的利器,但解锁操作如果出错,很容易导致死锁或者资源泄漏,核心原则是确保每一次锁的获取都对应一次释放,具体可以这么做:
用
try/finally包裹锁操作(必做)
不管你的代码在锁内会不会抛出异常,finally块都会执行,这是最可靠的确保锁被释放的方式。举个读锁的例子:var rwLock = new ReadWriterLockSlim(); rwLock.EnterReadLock(); try { // 在这里执行你的读操作逻辑 } finally { rwLock.ExitReadLock(); // 必须和Enter的锁类型对应 }写锁和可升级读锁同理,对应
EnterWriteLock()/ExitWriteLock()、EnterUpgradeableReadLock()/ExitUpgradeableReadLock(),绝对不能混用解锁方法,不然会抛出SynchronizationLockException。可以用自定义扩展实现
using自动释放
如果你觉得try/finally写起来繁琐,可以自己写个扩展方法,把锁包装成可释放的对象,这样就能用using语法自动解锁,代码更简洁:// 先定义一个简单的Disposable辅助类 public class DisposableAction : IDisposable { private readonly Action _action; public DisposableAction(Action action) => _action = action; public void Dispose() => _action?.Invoke(); } // 给ReadWriterLockSlim写扩展方法 public static IDisposable UseReadLock(this ReadWriterLockSlim rwLock) { rwLock.EnterReadLock(); return new DisposableAction(() => rwLock.ExitReadLock()); } // 使用方式 using (rwLock.UseReadLock()) { // 读操作逻辑 }写锁和可升级读锁也可以照葫芦画瓢做对应的扩展。
嵌套锁要严格对应顺序
如果必须嵌套使用锁(比如先读再升级为写),一定要保证每一次Enter都有对应的Exit,顺序不能乱。比如:rwLock.EnterUpgradeableReadLock(); try { // 读操作 rwLock.EnterWriteLock(); try { // 写操作 } finally { rwLock.ExitWriteLock(); } } finally { rwLock.ExitUpgradeableReadLock(); }
先把你给出的代码片段整理一下:
[System.Diagnostics.CodeAnalysis.SuppressMessage("Await.Warning", "CS4014:Await.Warning")] private async Task<bool> Refresh() { log.Info("Refreshing Token."); Debug.WriteLine("Refreshing Token."); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(TOKEN_URL); request.Method = "POST"; request.ContentType = "application/json; charset=UTF-8"; request.Headers.Add("Authorization", "basic " + authCode); request.Accept = "application/json, text/javascript, */*; q=0.01"; // 代码片段不完整,以下基于现有内容分析 }
我挑几个关键问题说说:
CS4014警告的处理方式不推荐
你用SuppressMessage压掉了CS4014警告,这个警告的原因是你在异步方法里调用了某个返回Task的方法但没有用await,直接把任务丢了。这种做法风险很高:- 如果那个未等待的任务抛出异常,在旧版.NET里会直接导致进程崩溃,新版虽然会吞掉异常,但你完全看不到错误日志,排查问题会非常困难。
- 正确的做法:如果不需要等待任务完成,至少要处理它的异常,比如:
如果可以等待,那就直接加_ = SomeAsyncMethod().ContinueWith(t => { if (t.Exception != null) { log.Error("异步方法执行失败", t.Exception); } }, TaskContinuationOptions.OnlyOnFaulted);await,不要随便压制警告。
使用
HttpWebRequest不如用HttpClientHttpWebRequest是比较老旧的API,在异步场景下,HttpClient的设计更符合现代异步编程习惯,代码更简洁,也更高效。比如你这段代码用HttpClient改写会是这样:private async Task<bool> Refresh() { log.Info("Refreshing Token."); Debug.WriteLine("Refreshing Token."); using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", authCode); // 假设你需要发送JSON请求体 var requestBody = new { /* 你的请求参数 */ }; var content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync(TOKEN_URL, content); // 后续处理响应、判断是否刷新成功... return response.IsSuccessStatusCode; }而且
HttpWebRequest的异步操作需要调用GetResponseAsync,如果你的代码后续没有正确用异步方式处理,会导致线程阻塞,浪费资源。代码功能不完整
目前的代码只创建了请求对象,没有写入POST请求需要的请求体,也没有获取响应内容、判断Token是否刷新成功的逻辑,这部分得补充完整,不然这个Refresh方法根本完成不了它的功能。日志和Debug输出的搭配是合理的
同时用log.Info(框架日志,生产环境也会输出)和Debug.WriteLine(仅Debug模式输出)是没问题的,既保证了生产环境的日志可追溯,也方便开发时调试。
内容的提问来源于stack exchange,提问作者Cliff Hill

