C# 日常开发中,资源管理是一个常见但容易出错的问题。无论是数据库连接、文件句柄还是信号量,忘记释放资源都可能导致内存泄漏、死锁等严重问题。
传统的 try-finally块虽然可靠,但代码冗长且容易遗漏。C# 提供了一个比较优雅的解决方案:使用using语句。但要让自定义资源支持 using,需要按照特定的模式。
无意中在ABP vNext框架中看到了人家的实现,作为笔记记录下来,下面将以 SemaphoreSlim为例,演示一下封装支持 using语法糖的自定义资源管理器。
一、问题的根源
考虑以下常见的信号量使用模式:
private readonly SemaphoreSlim _semaphore = new(1, 1);
private Guid _currentGuid = Guid.NewGuid();
public async Task AccessResourceAsync()
{
Console.WriteLine($"线程 {_currentGuid} 等待访问资源.");
bool isGet = await _semaphore.WaitAsync(1000);
if (!isGet)
{
Console.WriteLine($"线程 {_currentGuid} 指定时间内未获取资源,丢弃.");
return;
}
try
{
Console.WriteLine($"线程 {_currentGuid} 已经进入获取资源.");
// 执行关键业务逻辑
await DoCriticalWorkAsync();
}
finally
{
Console.WriteLine($"线程 {_currentGuid} 已经释放资源.");
_semaphore.Release();
}
}
这段代码存在几个问题:
1.模板代码过多:每次使用信号量都要写相同的获取/释放逻辑
2.容易出错:忘记写 finally块会导致死锁
3.可读性差:业务逻辑被资源管理代码淹没
二、理解 Using 的本质
C# 的 using语句本质上是一个语法糖,编译器会将其转换为标准的 try-finally 块:
// 你写的代码:
using (var resource = GetResource())
{
// 使用资源
}
// 编译器生成的代码:
var resource = GetResource();
try
{
// 使用资源
}
finally
{
if (resource != null)
((IDisposable)resource).Dispose();
}
关键点:
能被using包裹的必须是实现了 IDisposable接口的对象,且 Dispose()方法会在finally块中被调用
三、实现自定义 Using 支持
首先,我们需要使用委托来创建一个通用的包装器,将任意释放逻辑包装成 IDisposable:
public class DisposeAction : IDisposable
{
private readonly Action _action;
public DisposeAction(Action action)
{
_action = action;
}
public void Dispose()
{
_action?.Invoke();
}
}
1.使用SemaphoreSlim封装
public static class SemaphoreSlimExtension
{
public static async Task<IDisposable> LoctkAsync(this SemaphoreSlim semaphoreSlim, int duration, Guid guid)
{
Console.WriteLine($"线程 {guid} 等待访问资源.");
bool isGet = await semaphoreSlim.WaitAsync(1000);
if (!isGet)
{
Console.WriteLine($"线程 {guid} 指定时间内未获取资源,丢弃.");
return default(IDisposable);
}
Console.WriteLine($"线程 {guid} 已经进入获取资源.");
return GetDispose(semaphoreSlim, guid);
}
private static IDisposable GetDispose(this SemaphoreSlim semaphoreSlim, Guid guid)
{
return new DisposeAction(() =>
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new(9, 1);
defaultInterpolatedStringHandler.AppendLiteral("线程");
defaultInterpolatedStringHandler.AppendFormatted<Guid>(guid);
defaultInterpolatedStringHandler.AppendLiteral("已经释放资源.");
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
//Console.WriteLine($"线程{guid}已经释放资源.");
semaphoreSlim.Release();
});
}
}
四、实际使用示例
private readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task ProcessDataAsync()
{
// 优雅的 using 语法
using (await _semaphore.LockAsync(5000, "数据处理"))
{
// 临界区代码
Console.WriteLine("开始处理关键数据...");
await Task.Delay(1000);
Console.WriteLine("数据处理完成");
} // 这里会自动释放信号量
}
通过将自定义资源包装成 IDisposable,可以充分利用 C# 的 using语法糖,撸出安全优雅的代码。这种模式适用于信号量,当然还可以扩展到数据库连接、文件锁、事务管理等任何需要确定释放的资源。
浙公网安备 33010602011771号