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

C#中FileSystemWatcher非线程安全场景的处理方案咨询

问题: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();
    }
}

额外建议

  1. 避免在锁内执行耗时操作:比如CMDder.ExecuteCmd是调用系统命令,属于耗时操作,要放在锁外面完成,处理好结果后再进入锁更新lines,减少锁的持有时间,降低线程阻塞概率。
  2. Logs类的设计是合理的:将Watcher和文件内容放在同一对象符合单一职责原则,每个Logs实例对应一个监控文件的所有相关资源,便于管理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 08:33:28