c# 单例模式和DI容器在高并发下的生命周期管理

单例模式在高并发下不是自动线程安全的;手动实现需用Lazy、静态构造函数或双重检查锁,DI容器注册的Singleton仅在同一个IServiceProvider中全局唯一,且需避免状态竞争与生命周期混用。

单例模式在高并发下是否线程安全?

不是自动线程安全的。C# 中手动实现的 Singleton 类,如果没做同步控制(

比如没用 lockLazy 或双重检查锁),首次实例化时可能创建多个实例——尤其在多线程同时调用 Instance 属性时。

常见错误是这样写:

public class MySingleton
{
    private static MySingleton _instance;
    public static MySingleton Instance => _instance ??= new MySingleton();
}

上面的 ??= 在高并发下不是原子操作,_instance 可能被多次赋值。正确做法是用 Lazy

public class MySingleton
{
    private static readonly Lazy _lazy = new Lazy(() => new MySingleton());
    public static MySingleton Instance => _lazy.Value;
}
  • Lazy 默认启用线程安全模式(LazyThreadSafetyMode.ExecutionAndPublication
  • 避免手写双重检查锁,容易漏掉 volatile 或内存屏障
  • 静态构造函数也可保证线程安全,但无法延迟初始化

DI 容器注册为 Singleton 时,实例真的全局唯一吗?

是的,但前提是:你用的是同一个 IServiceProvider 实例(即同一个 DI 容器根容器)。ASP.NET Core 默认的 WebHostBuilder / HostBuilder 创建的是单根容器,所有请求共享同一组 singleton 实例。

容易踩的坑:

  • 在中间件或控制器里手动调用 services.BuildServiceProvider() → 每次都新建一个容器,导致 singleton 变成“伪单例”
  • ScopedTransient 服务中持有对 singleton 的引用没问题,但反过来——singleton 里依赖 Scoped 服务(如 DbContext)会引发异常或隐式捕获 scope
  • 使用第三方容器(如 Autofac、DryIoc)时,确认其 SingleInstance() / Singleton() 行为与 Microsoft.Extensions.DependencyInjection 一致

高并发下 singleton 服务里的状态管理风险

DI 容器只保证“实例单一”,不保证“线程安全”。如果你的 singleton 类里有可变字段(private int _counter)、缓存字典(ConcurrentDictionary 除外)、或未加锁的集合操作,就会出现数据竞争。

典型场景:

  • Dictionary 做运行时缓存 → 高并发读写直接抛 InvalidOperationException
  • 在 singleton 中缓存 HttpClient 是安全的(它本就是为复用设计),但缓存 HttpClientHandler 并手动设置 Credentials 等属性可能引发副作用
  • 异步方法中用 async void 或未 await 的 Task → 可能导致 singleton 状态错乱或资源泄漏

建议:

  • 优先用不可变对象、纯函数逻辑
  • 状态变更必须加锁(lockSemaphoreSlim)或改用线程安全集合(ConcurrentDictionaryConcurrentQueue
  • 避免在 singleton 中存储 request/session 级别数据(该用 Scoped

DI 容器和手写单例混用会出什么问题?

混合使用会导致生命周期失控。例如:你在 Startup.ConfigureServices 注册了 services.AddSingleton(),又在某个类里写了 MyService.Instance 手动单例,两个实例各自维护状态,行为完全割裂。

更隐蔽的问题:

  • 手写单例里依赖了 DI 容器注入的服务(比如通过 IServiceProvider 获取),但该 provider 是从 scoped service 拿的 → 生命周期越界
  • 单元测试时,手写单例无法被替换或重置,破坏可测性
  • 某些 DI 容器(如 Scrutor)支持装饰器、条件注册,手写单例绕过了这些机制

结论:在 ASP.NET Core 项目中,应统一走 DI 容器管理生命周期。手写单例仅限极少数场景(如配置解析器、日志门面封装),且不得参与依赖图。

最常被忽略的一点:singleton 服务的构造函数不能耗时或阻塞(比如连数据库、读大文件),否则会拖慢整个应用启动,甚至触发 Kestrel 启动超时。