使用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.30319Description: 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")读取配置文件时,和日志写入操作出现文件访问冲突:
- 配置刷新和日志写入的并发IO操作导致文件锁定冲突,可能是配置文件被日志操作间接占用,或者
RefreshSection内部读取时未正确处理文件共享模式; - FileSystem Watcher的事件回调在IO线程执行,直接在回调中执行同步日志写入,容易引发线程安全问题,且IO线程被阻塞会加剧并发冲突;
- 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

