在C#高并发系统中,依赖注入(DI)容器提供的三种生命周期Singleton、Scoped和Transient,决定了服务实例的创建频率与共享范围。选错生命周期可能导致线程冲突、内存暴涨或请求数据串号。理解它们的本质差异,是做对选择的前提。

三种生命周期的基本行为
Singleton在整个应用生命周期内只创建一个实例,所有请求和线程共享;Scoped在一次请求或一次显式子容器中创建一个实例;Transient每次从容器解析都生成新对象。
- Singleton:全局单例,适合无状态服务。
- Scoped:请求级实例,适合需要请求内共享的组件。
- Transient:每次新建,适合轻量且无共享需求的组件。
高并发下的核心判断维度
1. 是否有可变状态
如果服务保存了可被修改的字段,且会被多个线程访问,使用Singleton就必须加锁或采用线程安全结构。无状态的计算服务则可放心用Singleton。
2. 是否持有请求相关上下文
如EF Core的DbContext通常应为Scoped,因为它跟踪当前请求的数据变更。若在Singleton中持有Scoped服务,会造成内存泄漏。
3. 资源开销与GC压力
Transient在高并发下会频繁创建对象,若服务构造昂贵(如建立数据库连接),会压垮GC和系统吞吐。
典型场景代码示例
以下示例展示在ASP.NET Core中注册不同生命周期,并在后台任务中错误使用Scoped的情况:
// 注册配置
public void ConfigureServices(IServiceCollection services)
{
// 无状态缓存服务,高并发推荐Singleton
services.AddSingleton<ICacheService, MemoryCacheService>();
// 数据库上下文,应Scoped
services.AddDbContext<AppDbContext>();
// 轻量日志器,可用Transient
services.AddTransient<ILogger, FileLogger>();
}
// 错误示例:在Singleton服务中直接注入Scoped的DbContext
public class BadSingletonService
{
private readonly AppDbContext _ctx;
public BadSingletonService(AppDbContext ctx) // 捕获Scoped实例到Singleton中
{
_ctx = ctx;
}
}
并行任务中的正确做法
在后台Worker或Parallel.For中,应创建Scope再解析服务:
public class Worker
{
private readonly IServiceProvider _provider;
public Worker(IServiceProvider provider)
{
_provider = provider;
}
public void Process()
{
// 每个并行项使用独立Scope,避免跨线程共享Scoped
Parallel.For(0, 100, i =>
{
using var scope = _provider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 安全使用db
});
}
}
选择建议速查表
| 服务特征 | 推荐生命周期 | 原因 |
|---|---|---|
| 无状态、线程安全、被频繁使用 | Singleton | 减少分配,提升吞吐 |
| 依赖请求数据、需事务一致 | Scoped | 隔离请求,防止串数据 |
| 构造轻量、无共享、短时使用 | Transient | 避免状态污染 |
| 持有非线程安全资源且长期运行 | Scoped或Transient | 禁止Singleton防止冲突 |
总结
高并发下选DI生命周期,先问自己:它有没有状态、归谁所有、活多久。无状态且全局复用选Singleton;请求内共享选Scoped;不确定或需强隔离选Transient。切忌把Scoped塞进Singleton,也别用Transient包裹重型资源。