c# 如何防止ASP.NET Core应用在启动时并发执行初始化代码

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

使用
Lazy<t></t>
包裹单例初始化逻辑

ASP.NET Core 默认使用多线程启动(尤其在 Kestrel + 多核 CPU 下),

Program.cs
Startup.ConfigureServices
中的静态初始化、静态字段赋值、或单例服务的构造函数,都可能被多个线程同时触发。直接在构造函数里写耗时初始化(如加载配置、连接数据库、预热缓存)会导致重复执行甚至竞态错误。

Lazy<t></t>
是 .NET 原生线程安全的延迟初始化机制,配合
LazyThreadSafetyMode.ExecutionAndPublication
可确保仅一次执行且结果对所有线程可见。

public class ExpensiveInitializer
{
    private static readonly Lazy<ExpensiveInitializer> _instance = 
        new Lazy<ExpensiveInitializer>(() => new ExpensiveInitializer(), 
            LazyThreadSafetyMode.ExecutionAndPublication);
    public static ExpensiveInitializer Instance => _instance.Value;
    private ExpensiveInitializer()
    {
        // 这段代码只会被执行一次,即使多个线程同时首次访问 Instance
        Console.WriteLine("Initializing...");
        Thread.Sleep(1000); // 模拟耗时操作
    }
}
必须显式指定
LazyThreadSafetyMode.ExecutionAndPublication
,否则默认模式在某些 .NET 版本下可能不保证构造函数只调一次
不要把
Lazy<t></t>
实例放在非静态字段中——那会失去“全局唯一初始化”的意义
如果初始化过程可能抛异常,
Lazy<t></t>
会缓存异常,后续访问直接重抛;需自行捕获并处理或改用
AsyncLazy<t></t>

IServiceProvider
中注册时使用工厂委托 + 锁保护

当初始化逻辑依赖 DI 容器(比如需要

IConfiguration
ILogger
),就不能用纯静态
Lazy<t></t>
,而应在
ConfigureServices
中用工厂方法注册,并手动加锁控制首次执行。

注意:不能用

lock
锁住
this
或类型对象(如
typeof(MyService)
),因为此时
IServiceCollection
还未构建完成,锁对象可能不唯一;推荐用私有静态对象。

private static readonly object _initLock = new object();
private static bool _initialized = false;
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IMyService>(sp =>
    {
        lock (_initLock)
        {
            if (!_initialized)
            {
                var config = sp.GetRequiredService<IConfiguration>();
                var logger = sp.GetRequiredService<ILogger<MyService>>();
                // 执行初始化:读配置、建连接池、预热等
                logger.LogInformation("Running one-time init...");
                InitializeOnce(config);
                _initialized = true;
            }
        }
        return new MyService();
    });
}
该方式适用于初始化必须依赖 DI 服务的场景,但要注意:锁会阻塞其他服务注册线程,应尽量缩短临界区(只包真正需要同步的部分) 避免在锁内调用异步方法或等待 I/O(如
await
),否则会阻塞整个容器构建流程
如果初始化本身是异步的(如
await LoadCacheAsync()
),需改用
AsyncLazy<t></t>
或信号量(
SemaphoreSlim
)+ 异步锁模式

警惕
IHostedService.StartAsync
的并发调用风险

很多开发者误以为把初始化移到

IHostedService
就天然串行,其实 ASP.NET Core 会并发调用所有已注册的
IHostedService.StartAsync
——除非你显式控制顺序或同步。

常见错误:多个

IHostedService
都尝试初始化同一资源(如 Redis 连接、内存缓存项),导致重复连接或覆盖。

若必须用
IHostedService
,应在实现类内部用
Lazy<task></task>
AsyncLazy<t></t>
包装初始化逻辑
不要在
StartAsync
中直接写
if (!_inited) { DoInit(); _inited = true; }
—— 多个实例可能同时通过判断,造成竞态
更稳妥的做法是:让初始化逻辑集中在一个专用的
IHostedService
中,并通过
IServiceProvider
注入它所初始化的资源,其他服务只消费不初始化

为什么不用
ConcurrentDictionary.GetOrAdd

有人尝试用

ConcurrentDictionary<string object>.GetOrAdd("init", _ => { ... })</string>
实现一次性初始化,这看似可行,但存在隐患:

GetOrAdd
的 valueFactory 可能被多次调用(虽然只有一次胜出),若初始化逻辑有副作用(如发 HTTP 请求、写日志、修改静态状态),就会意外触发多次
无法区分“初始化成功”和“初始化失败后重试”,异常会被吞掉或重复抛出 不如
Lazy<t></t>
语义清晰、行为确定,.NET 团队也明确推荐
Lazy<t></t>
用于一次性初始化场景

真正需要防止并发初始化的地方,核心就两条:用对机制(

Lazy<t></t>
或带锁工厂),并且把“是否已完成”的状态绑定到线程安全的存储上——而不是靠条件判断或字典试探。

相关推荐