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

使用FileSystem Watcher动态刷新日志级别触发System.IO异常求助

动态刷新日志级别时并发写入导致崩溃的排查与解决

我需要无需重启应用,通过修改配置文件重新加载日志级别,于是用FileSystem Watcher监控配置文件变化,实现代码如下:

static void OnConfigFileChanged()
{
    ConfigurationManager.RefreshSection("appSettings");

    int newLogLevel = int.Parse(ConfigurationManager.AppSettings["LogLevel"].ToString());

    if (SystemParamsDto.LogLevel != newLogLevel)
    {
        SystemParamsDto.LogLevel = newLogLevel;
        LogWriter.applicationLogLevel = (LoggingLevel)SystemParamsDto.LogLevel;
        LogWriter.WriteLog(new LogDto() { FunctionName = "OnConfigFileChanged", LogStringLevel = LoggingLevel.Exceptional, AdditionalInfo = $"LogLevel updated to {LogWriter.applicationLogLevel}", Message = "LogLevel_change" });
    }
}

修改配置文件后,日志级别更新的日志能正常显示,但应用同时执行日志写入操作时,会触发未处理异常导致崩溃,异常信息如下:

Application: xxx.exe

Framework Version: v4.0.30319

Description: The process was terminated due to an unhandled exception.

Exception Info: System.IO.IOException

at System.IO.__Error.WinIOError(Int32, System.String)

at System.IO.FileStream.Init(System.String, System.IO.FileMode, System.IO.FileAccess, Int32, Boolean, System.IO.FileShare, Int32, System.IO.FileOptions, SECURITY_ATTRIBUTES, System.String, Boolean, Boolean, Boolean)

at System.IO.FileStream..ctor(System.String, System.IO.FileMode, System.IO.FileAccess, System.IO.FileShare, Int32, System.IO.FileOptions, System.String, Boolean)

at System.IO.FileStream..ctor(System.String, System.IO.FileMode, System.IO.FileAccess, System.IO.FileShare)

at System.Configuration.Internal.InternalConfigHost.StaticOpenStreamForRead(System.String)

at System.Configuration.Internal.InternalConfigHost.System.Configuration.Internal.IInternalConfigHost.OpenStreamForRead(System.String, Boolean)

at System.Configuration.BaseConfigurationRecord.RefreshFactoryRecord(System.String)

Exception Info: System.Configuration.ConfigurationErrorsException

at System.Configuration.ConfigurationSchemaErrors.ThrowIfErrors(Boolean)

at System.Configuration.BaseConfigurationRecord.ThrowIfParseErrors(System.Configuration.ConfigurationSchemaErrors)

at System.Configuration.BaseConfigurationRecord.hlClearResultRecursive(System.String, Boolean)

at System.Configuration.BaseConfigurationRecord.hlClearResultRecursive(System.String, Boolean)

at System.Configuration.Internal.InternalConfigRoot.ClearResult(System.Configuration.BaseConfigurationRecord, System.String, Boolean)

at System.Configuration.ClientConfigurationSystem.System.Configuration.Internal.IInternalConfigSystem.RefreshConfig(System.String)

at Beanbag.DataProcessor.Controller.DataProcessorC. ()

at System.IO.FileSystemWatcher.OnChanged(System.IO.FileSystemEventArgs)

at System.IO.FileSystemWatcher.CompletionStatusChanged(UInt32, UInt32, System.Threading.NativeOverlapped*)

at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32, UInt32, System.Threading.NativeOverlapped*)

问题分析

从异常栈可以看出,崩溃根源是System.IO.IOException,发生在ConfigurationManager.RefreshSection("appSettings")读取配置文件时,和日志写入操作出现文件访问冲突:

  1. 配置刷新和日志写入的并发IO操作导致文件锁定冲突,可能是配置文件被日志操作间接占用,或者RefreshSection内部读取时未正确处理文件共享模式;
  2. FileSystem Watcher的事件回调在IO线程执行,直接在回调中执行同步日志写入,容易引发线程安全问题,且IO线程被阻塞会加剧并发冲突;
  3. FileSystem Watcher可能因配置文件的多次修改触发重复回调,进一步放大并发问题。

解决方案

1. 给配置刷新逻辑加线程锁并捕获异常

用锁确保同一时间只有一个线程执行配置刷新,同时捕获IO和配置解析异常,避免未处理异常导致崩溃,异步执行日志写入避免阻塞IO线程:

private static readonly object _configLock = new object();

static void OnConfigFileChanged()
{
    lock (_configLock)
    {
        try
        {
            ConfigurationManager.RefreshSection("appSettings");
            string logLevelStr = ConfigurationManager.AppSettings["LogLevel"];
            
            if (!int.TryParse(logLevelStr, out int newLogLevel))
            {
                Task.Run(() => 
                    LogWriter.WriteLog(new LogDto() { 
                        FunctionName = "OnConfigFileChanged", 
                        LogStringLevel = LoggingLevel.Warning, 
                        AdditionalInfo = $"Invalid LogLevel value: {logLevelStr}", 
                        Message = "LogLevel_change_failed" 
                    })
                );
                return;
            }

            if (SystemParamsDto.LogLevel != newLogLevel)
            {
                SystemParamsDto.LogLevel = newLogLevel;
                LogWriter.applicationLogLevel = (LoggingLevel)SystemParamsDto.LogLevel;
                
                // 异步写日志,避免阻塞IO线程
                Task.Run(() => 
                    LogWriter.WriteLog(new LogDto() { 
                        FunctionName = "OnConfigFileChanged", 
                        LogStringLevel = LoggingLevel.Exceptional, 
                        AdditionalInfo = $"LogLevel updated to {LogWriter.applicationLogLevel}", 
                        Message = "LogLevel_change" 
                    })
                );
            }
        }
        catch (IOException ex)
        {
            Task.Run(() => 
                LogWriter.WriteLog(new LogDto() { 
                    FunctionName = "OnConfigFileChanged", 
                    LogStringLevel = LoggingLevel.Error, 
                    AdditionalInfo = $"Failed to refresh config: {ex.Message}", 
                    Message = "Config_refresh_failed" 
                })
            );
            // 延迟重试,避免立即重试再次触发冲突
            Task.Delay(100).ContinueWith(_ => OnConfigFileChanged());
        }
        catch (ConfigurationErrorsException ex)
        {
            Task.Run(() => 
                LogWriter.WriteLog(new LogDto() { 
                    FunctionName = "OnConfigFileChanged", 
                    LogStringLevel = LoggingLevel.Error, 
                    AdditionalInfo = $"Config parse error: {ex.Message}", 
                    Message = "Config_parse_failed" 
                })
            );
        }
    }
}

2. 给FileSystem Watcher事件加防抖处理

避免因配置文件多次修改触发重复回调,设置延迟执行逻辑,仅在最后一次事件触发后执行配置刷新:

private static Timer _debounceTimer;
private static readonly TimeSpan _debounceDelay = TimeSpan.FromMilliseconds(200);

// 修改Watcher的事件处理方法
static void OnConfigFileChanged(object sender, FileSystemEventArgs e)
{
    // 重置定时器,仅在最后一次修改后延迟执行
    _debounceTimer?.Dispose();
    _debounceTimer = new Timer(_ => 
    {
        RefreshLogLevelConfig();
        _debounceTimer.Dispose();
        _debounceTimer = null;
    }, null, _debounceDelay, Timeout.InfiniteTimeSpan);
}

// 将原配置刷新逻辑抽离到单独方法
private static void RefreshLogLevelConfig()
{
    // 放入上述加锁的配置刷新代码
}

3. 优化日志写入的文件共享模式

检查LogWriter.WriteLog内部的文件操作,确保打开日志文件时使用FileShare.ReadWrite,避免独占文件引发的系统级锁定冲突:

// 日志写入示例(替换实际的日志文件操作逻辑)
public static void WriteLog(LogDto logDto)
{
    string logFilePath = "your_log_file_path.log";
    // 使用FileShare.ReadWrite允许其他线程读取/写入文件
    using (var fs = new FileStream(logFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
    using (var sw = new StreamWriter(fs))
    {
        sw.WriteLine($"{DateTime.Now}: {logDto.Message} - {logDto.AdditionalInfo}");
    }
}

4. 避免在IO线程执行阻塞操作

FileSystem Watcher的回调运行在系统IO线程池线程中,不要在回调内执行同步IO或长时间操作,尽量通过Task.Run将逻辑放到业务线程池处理,避免耗尽IO线程资源。

内容的提问来源于stack exchange,提问作者Mudita jain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 21:44:54