C#中FileSystemWatcher非线程安全场景的处理方案咨询
背景
Stack Overflow上有大量关于FileSystemWatcher线程安全的讨论,普遍结论是其本身线程安全,仅在和GUI集成时需要处理线程切换问题。但我遇到一个无法判断是否线程安全的场景,寻求建议。
场景与代码
我用FileSystemWatcher监控一个纯文本日志文件,文件变更时读取最新内容并存储到变量/对象中。相关代码如下:
文件变更回调与内容更新代码
private void onChangeSystem(object sender, FileSystemEventArgs e) { tailFileCmd(ref system, 5); } private void tailFileCmd(ref Logs log, int tailAmount = 75) { if (log.safeToRead) { log.lines.Clear(); var result = CMDder.ExecuteCmd("tail -" + tailAmount.ToString() + " " + log.location); if (!result.HasError && result.ExitCode == 0) { var split = result.Result.Split('\n'); if (split.Length > 0) { foreach (string line in split) { log.lines.Add(line); } } } } }
Logs类定义
为了处理多个文件,我创建了Logs类存储每个文件的相关数据(包括独立的Watcher),代码如下:
public class Logs { public logType type = logType.NOT_SET; public string location = string.Empty; public StreamReader? file; public FileSystemWatcher watcher = new FileSystemWatcher(); public List<string> lines = new List<string>(); public bool safeToRead = false; }
对象初始化时,若文件路径正确且StreamReader、FileSystemWatcher初始化成功,会将safeToRead设为true。CMDder是自定义类,用于执行系统命令;StreamReader为后续使用预留。目前代码能保持lines列表与文件最新内容同步,仅在文件变更时更新。
核心问题
另有一个函数会定期读取Logs对象中的lines数据并传递到其他地方,我认为当前实现并非线程安全(但难以复现问题)。如果判断正确,该如何修改代码实现线程安全,让lines的更新与读取操作互斥?我对C#线程经验较少,仅了解C++线程。
补充说明:我的代码不会修改目标文件,文件由其他程序修改。此处的“写入”指将文件内容存储为字符串列表lines。
你的判断是对的,当前代码确实存在线程安全问题:List<string>本身不是线程安全集合,当onChangeSystem回调(在FileSystemWatcher的线程池线程执行)修改lines时,若另一个线程同时读取lines,可能会引发异常(比如枚举时集合被修改)或读取到不完整的数据。
下面提供几种可行的修改方案:
方案一:使用lock语句实现互斥
这是最直接的方式,通过锁确保同一时间只有一个线程能访问lines。
修改Logs类
首先在Logs类中添加一个专用的锁对象(不要用this或lines本身作为锁对象,避免意外的锁竞争):
public class Logs { public logType type = logType.NOT_SET; public string location = string.Empty; public StreamReader? file; public FileSystemWatcher watcher = new FileSystemWatcher(); public List<string> lines = new List<string>(); public bool safeToRead = false; // 添加专用锁对象 private readonly object _lockObj = new object(); }
修改更新lines的代码
在tailFileCmd方法中,先在锁外处理好新数据,再进入锁更新lines,减少锁持有时间:
private void tailFileCmd(ref Logs log, int tailAmount = 75) { if (log.safeToRead) { var result = CMDder.ExecuteCmd("tail -" + tailAmount.ToString() + " " + log.location); if (!result.HasError && result.ExitCode == 0) { var split = result.Result.Split('\n'); var newLines = new List<string>(); if (split.Length > 0) { foreach (string line in split) { newLines.Add(line); } } // 加锁更新lines lock (log._lockObj) { log.lines.Clear(); log.lines.AddRange(newLines); } } } }
修改读取lines的代码
在定期读取lines的函数中,同样加锁并返回列表副本,避免外部代码持有对原列表的引用:
// 示例读取函数 public List<string> GetLogLines(Logs log) { lock (log._lockObj) { return new List<string>(log.lines); } }
方案二:使用读写锁ReaderWriterLockSlim
如果读取操作远多于写入操作,使用ReaderWriterLockSlim可以提升性能——允许多个线程同时读取,仅在写入时独占锁。
修改Logs类
public class Logs { // ... 其他字段不变 private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); }
修改更新代码
private void tailFileCmd(ref Logs log, int tailAmount = 75) { if (log.safeToRead) { var result = CMDder.ExecuteCmd("tail -" + tailAmount.ToString() + " " + log.location); if (!result.HasError && result.ExitCode == 0) { var split = result.Result.Split('\n'); var newLines = new List<string>(); if (split.Length > 0) { foreach (string line in split) { newLines.Add(line); } } // 获取写入锁 log._rwLock.EnterWriteLock(); try { log.lines.Clear(); log.lines.AddRange(newLines); } finally { // 确保释放锁 log._rwLock.ExitWriteLock(); } } } }
修改读取代码
public List<string> GetLogLines(Logs log) { log._rwLock.EnterReadLock(); try { return new List<string>(log.lines); } finally { log._rwLock.ExitReadLock(); } }
额外建议
- 避免在锁内执行耗时操作:比如
CMDder.ExecuteCmd是调用系统命令,属于耗时操作,要放在锁外面完成,处理好结果后再进入锁更新lines,减少锁的持有时间,降低线程阻塞概率。 - Logs类的设计是合理的:将Watcher和文件内容放在同一对象符合单一职责原则,每个Logs实例对应一个监控文件的所有相关资源,便于管理。
内容的提问来源于stack exchange,提问作者Deoxys_0

