You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core中IOptionsMonitor<T>依赖其他选项时的竞态条件处理

IOptionsMonitor依赖其他选项时的竞态问题及无ConfigurationRoot解决方案

问题场景

在服务中使用依赖其他选项的IOptionsMonitor<T>时,OnChange事件出现竞态条件:配置重载时选项异步初始化,导致依赖方选项(如SecondOptions)在被依赖选项(如FirstOptions)完成更新前就完成初始化,最终OnChange事件中会先出现旧值,再出现新值,产生延迟。

示例代码

public class FirstOptions
{
    public string FirstValue { get; set; }
}

public class SecondOptions
{
    public string SecondValue { get; set; }
}

internal class Worker : IHostedService
{
    private readonly IDisposable _disposable;
    private readonly string _filePath = "Config.ini";

    public Worker(IOptionsMonitor<SecondOptions> options)
    {
        Console.WriteLine($"Ctor: \"{options.CurrentValue.SecondValue}\"");
        _disposable = options.OnChange(OnOptionsChanged);
    }

    private void OnOptionsChanged(SecondOptions options, string? arg2)
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} OnChange: Second: {options.SecondValue}");
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        for (int i = 0; i < 20; i++)
        {
            await Task.Delay(2000);
            WriteFirstValue(_filePath, i.ToString());
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    private static void WriteFirstValue(string filePath, string value)
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Property changed to " + value);
        File.WriteAllText(filePath,
            "[FirstSection]"
            + Environment.NewLine + $"FirstValue=\"{value}\"");
    }
}

internal class Program
{
    private static void Main(string[] args)
    {
        IConfigurationRoot configurationRoot = new ConfigurationBuilder()
            .SetBasePath(Environment.CurrentDirectory)
            .AddIniFile("config.ini", false, true)
            .Build();

        var provider = configurationRoot.Providers.OfType<IniConfigurationProvider>().First();

        IHostBuilder builder = Host.CreateDefaultBuilder(args);
        builder.ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
            services.AddOptions<FirstOptions>().Bind(configurationRoot.GetSection("FirstSection"));
            services.AddOptions<SecondOptions>()
            .Bind(configurationRoot.GetSection("SecondSection"))
            .Configure<IOptionsMonitor<FirstOptions>>((secondOpt, firstOpt) => secondOpt.SecondValue = firstOpt.CurrentValue.FirstValue);
            //.Configure(secondOpt => secondOpt.SecondValue = configurationRoot.GetSection("FirstSection").GetValue<string>("FirstValue"));
        });

        IHost host = builder.Build();
        host.Run();
    }
}

异常输出

15:53:00.677 Property changed to 0
15:53:00.957 OnChange: Second: 0
15:53:01.222 OnChange: Second: 0
15:53:02.703 Property changed to 1
15:53:02.970 OnChange: Second: 0
15:53:03.222 OnChange: Second: 1
15:53:04.713 Property changed to 2
15:53:04.975 OnChange: Second: 1
15:53:05.228 OnChange: Second: 2
15:53:06.727 Property changed to 3
15:53:06.994 OnChange: Second: 2
15:53:07.259 OnChange: Second: 3

解决方案(不依赖ConfigurationRoot)

问题根源是配置变更时,FirstOptions和SecondOptions的重新初始化是并行的,导致SecondOptions可能先完成初始化并拿到旧值。我们可以通过监听FirstOptions的变更,主动清除SecondOptions的缓存,迫使它在FirstOptions更新完成后重新初始化,确保拿到最新值。

修改ConfigureServices代码

在服务注册中添加一个后台服务,监听FirstOptions的变更并清除SecondOptions的缓存:

builder.ConfigureServices(services =>
{
    services.AddHostedService<Worker>();
    services.AddOptions<FirstOptions>().Bind(configurationRoot.GetSection("FirstSection"));
    services.AddOptions<SecondOptions>()
        .Bind(configurationRoot.GetSection("SecondSection"))
        .Configure<IOptionsMonitor<FirstOptions>>((secondOpt, firstOpt) => 
            secondOpt.SecondValue = firstOpt.CurrentValue.FirstValue);

    // 添加监听FirstOptions变更、清除SecondOptions缓存的后台服务
    services.AddSingleton<IHostedService>(sp =>
    {
        var firstMonitor = sp.GetRequiredService<IOptionsMonitor<FirstOptions>>();
        var secondCache = sp.GetRequiredService<IOptionsMonitorCache<SecondOptions>>();
        var disposable = firstMonitor.OnChange((_, __) => 
            secondCache.TryRemove(Options.DefaultName));
        
        return new CacheCleanupBackgroundService(disposable);
    });
});

添加缓存清理后台服务类

在Program类内部添加以下嵌套类:

private class CacheCleanupBackgroundService : IHostedService
{
    private readonly IDisposable _changeSubscription;

    public CacheCleanupBackgroundService(IDisposable changeSubscription)
    {
        _changeSubscription = changeSubscription;
    }

    public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _changeSubscription.Dispose();
        return Task.CompletedTask;
    }
}

原理说明

  • 当FirstOptions发生变更时,IOptionsMonitor<FirstOptions>的OnChange事件会触发,调用IOptionsMonitorCache<SecondOptions>.TryRemove清除SecondOptions的默认实例缓存。
  • 缓存清除后,下一次访问SecondOptions(包括OnChange触发的重新初始化)会重新执行配置逻辑,此时FirstOptions已经完成更新,能拿到最新的FirstValue。
  • 最终SecondOptions的OnChange事件只会触发一次,且值为最新的FirstValue。

补充说明

ASP.NET 5.0文档曾提示“不要在Startup.ConfigureServices中使用IOptions或IOptionsMonitor,服务注册顺序可能导致选项状态不一致”,虽然该说明在后续版本文档中移除,但本质问题是选项初始化的顺序和异步特性导致的,上述方案通过主动控制缓存生命周期,规避了竞态条件。

内容的提问来源于stack exchange,提问作者Filip Procházka

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 02:15:04