c# 在高并发下,如何选择合适的 DI 生命周期(Singleton, Scoped, Transient)

来源:这里教程网 时间:2026-02-21 17:37:05 作者:

Singleton 实例在高并发下必须线程安全

Singleton 服务在整个应用生命周期内只创建一次,所有请求共享同一个实例。这意味着如果你的类里有可变状态(比如

private List<string> _cache</string>
),多个线程同时写入就会触发竞态条件,轻则数据错乱,重则抛出
InvalidOperationException
NullReferenceException

常见错误现象:缓存数据突然消失、计数器值跳变、日志输出重复或缺失。

确保所有字段只读(
readonly
)或使用线程安全类型(如
ConcurrentDictionary<tkey tvalue></tkey>
ConcurrentQueue<t></t>
避免在 Singleton 中持有
HttpContext
DbContext
或任何与请求上下文强绑定的对象
如果必须做状态管理,优先用外部存储(Redis、内存队列)而非类内字段

Scoped 服务不是“每个线程一个”,而是“每个请求一个”

Scoped
生命周期由 DI 容器根据当前作用域决定,在 ASP.NET Core 中默认绑定到
HttpRequest
。它**不等于线程局部存储(TLS)**,也不随线程切换而隔离——异步操作中若跨
await
后仍在同一请求上下文中,仍会拿到同一个 Scoped 实例;但若在后台线程(如
Task.Run
)中手动创建新作用域,则会得到新实例。

容易踩的坑:

HostedService
或定时任务中直接注入 Scoped 服务 → 抛出
InvalidOperationException: Cannot resolve scoped service...
IServiceScopeFactory.CreateScope()
手动创建作用域后忘记调用
scope.Dispose()
→ 导致
DbContext
连接泄漏、内存缓慢增长
把 Scoped 服务注入到 Singleton 类构造函数中 → 容器启动失败,报错
Cannot consume scoped service...

Transient 适合无状态、轻量、依赖隔离强的类型

Transient
每次请求都新建实例,开销小但不为零。高频调用下(如每毫秒调用几十次的工具类),对象分配+GC 压力会上升,尤其当类内部持有大数组或未释放的非托管资源时。

适用场景:

DTO 映射器(如
IMapper
的自定义转换器)
纯函数式工具类(无字段、只暴露静态方法的类别走 DI,但若已注册为 Transient,也无妨) 需要严格隔离状态的处理器(例如每个请求都要独立初始化加密上下文)

反例:把

HttpClient
注册为 Transient —— 会导致端口耗尽。应改为 Singleton +
IHttpClientFactory
管理。

混合生命周期组合要检查依赖图是否越级引用

DI 容器禁止将短生命周期服务注入长生命周期服务,例如:把 Scoped 服务注入 Singleton 构造函数。编译期不报错,运行时容器构建阶段就失败,错误信息类似:

System.InvalidOperationException: Cannot consume scoped service 'MyApp.Services.IUserService' from singleton 'MyApp.Services.INotificationService'.

排查建议:

dotnet trace
或第三方库(如 Scrutor)扫描注册关系
Program.cs
中启用验证:
services.AddControllers().AddControllersAsServices();
+
hostBuilder.UseDefaultServiceProvider(... ValidateOnBuild = true)
对复杂依赖链,优先把状态上提到外层(如用
AsyncLocal<t></t>
存请求 ID),而非靠 Scoped 传递上下文

真正麻烦的是隐式依赖:某个 Transient 类内部 new 出了一个 Scoped 类,或者通过反射加载了未注册的类型——这种不会被容器校验,只能靠压测暴露。

相关推荐