有梦想的鱼
写代码我得再认真点,当我最终放下键盘时,我实在不想仍有太多疑惑

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语法糖,撸出安全优雅的代码。这种模式适用于信号量,当然还可以扩展到数据库连接、文件锁、事务管理等任何需要确定释放的资源。

posted on 2026-05-25 17:33  yuyuyui  阅读(12)  评论(0)    收藏  举报