FileSystemWatcher的Changed事件被触发两次

390

我有一个应用程序,在其中寻找一个文本文件,如果文件有任何更改,我将使用 OnChanged 事件处理程序来处理该事件。我正在使用 NotifyFilters.LastWriteTime 但是仍然会触发两次事件。以下是代码。

public void Initialize()
{
   FileSystemWatcher _fileWatcher = new FileSystemWatcher();
  _fileWatcher.Path = "C:\\Folder";
  _fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
  _fileWatcher.Filter = "Version.txt";
  _fileWatcher.Changed += new FileSystemEventHandler(OnChanged);
  _fileWatcher.EnableRaisingEvents = true;
}

private void OnChanged(object source, FileSystemEventArgs e)
{
   .......
}
在我的情况下,当我改变文本文件version.txt并保存它时,OnChanged被调用了两次。

这是一个解决方法,但应该根据解决方法的质量来评判。跟踪更改效果完美且简单。OP正在寻找一种抑制重复事件的方法,下面的回复就提供了这种方法。https://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.created.aspx解释了多个事件可能是由于反病毒软件或其他“复杂的文件系统问题”引起的(听起来只是借口)。 - Tyler Montney
2
我最近打开了这个问题 https://github.com/Microsoft/dotnet/issues/347 - Stephan Ahlf
3
我创建了一个类,可以帮助您获取一个事件。您可以从https://github.com/melenaos/FileSystemSafeWatcher获取代码。 - Menelaos Vergis
上面Menelaos Vergis提供的解决方案百分之百有效。 - undefined
45个回答

305
我担心这是FileSystemWatcher类的一个众所周知的bug/特性。这是该类的文档中的内容:

在某些情况下,您可能会注意到单个创建事件会生成多个由您的组件处理的已创建事件。例如,如果您使用FileSystemWatcher组件来监视目录中新文件的创建,并使用记事本创建文件进行测试,您可能会看到生成了两个已创建事件,尽管只创建了一个文件。这是因为记事本在写入过程中执行多个文件系统操作。记事本批量写入磁盘,创建文件的内容,然后是文件属性。其他应用程序可能以相同的方式执行。因为FileSystemWatcher监视操作系统活动,所以将捕获这些应用程序触发的所有事件。

现在这段文字是关于Created事件的,但同样适用于其他文件事件。在某些应用程序中,您可能可以通过使用NotifyFilter属性来解决此问题,但我的经验表明,有时您还需要进行一些手动的重复过滤(hack)。
前段时间我收藏了一个带有一些FileSystemWatcher技巧的页面。你可能想看一下。

9
Raymond Chen刚刚在他的博客中写到:为什么在记事本中保存文件会触发多个FindFirstChangeNotification事件? - Cody Gray
2
一个不错的解决方案:FileSystemWatcher存在一些问题FileSystemWatcherMemoryCache示例,作者是Ben Hall。 - gresolio
刚刚测试了一下,用记事本编辑只产生一个事件,而用Notepad++编辑却产生两个事件。 - Legends

162

我已经在我的代理中使用以下策略“解决”了那个问题:

// fsw_ is the FileSystemWatcher instance used by my application.

private void OnDirectoryChanged(...)
{
   try
   {
      fsw_.EnableRaisingEvents = false;

      /* do my stuff once asynchronously */
   }

   finally
   {
      fsw_.EnableRaisingEvents = true;
   }
}

17
我尝试过这个方法,如果我一次只修改一个文件,那么它可以正常工作,但是如果我一次修改两个文件(例如将1.txt和2.txt复制到1.txt的副本和2.txt的副本),它只会引发一个事件,而不是像预期的那样引发两个事件。 - Christopher Painter
3
已经过去了几个月,但我认为我最终做的是使事件调用一个将业务逻辑放在锁语句中的方法。这样,如果我收到额外的事件,它们会排队等待它们的机会,因为上一次迭代已经处理了一切,所以它们没有任何事情可做。 - Christopher Painter
18
这似乎解决了问题,但事实并非如此。如果另一个进程正在进行更改,则可能会丢失这些更改。看起来它能够工作是因为另一个进程的IO是异步的,并且在完成处理之前禁用了监视,从而与其他可能感兴趣的事件创建了竞争条件。这就是@ChristopherPainter观察到他的问题的原因。 - Jf Beaulac
20
如果在您被禁用时发生了另一种您感兴趣的更改,该怎么办? - G. Stoynev
3
除非你异步地完成"你的事情"。 - David Brabant
显示剩余6条评论

117

通过检查相应文件的File.GetLastWriteTime时间戳,可以检测和丢弃任何重复的FileSystemWatcherOnChanged事件。像这样:

DateTime lastRead = DateTime.MinValue;

void OnChanged(object source, FileSystemEventArgs a)
{
    DateTime lastWriteTime = File.GetLastWriteTime(uri);
    if (lastWriteTime != lastRead)
    {
        doStuff();
        lastRead = lastWriteTime;
    }
    // else discard the (duplicated) OnChanged event
}

14
我喜欢那个解决方案,但我已经使用 Rx 来做“正确”的事情(将“Rename”更改为您感兴趣的事件名称):Observable.FromEventPattern<FileSystemEventArgs>(fileSystemWatcher, "Renamed") .Select(e => e.EventArgs) .Distinct(e => e.FullPath) .Subscribe(onNext); - Kjellski
6
我有所遗漏吗?我不明白这将如何运作。根据我的观察,这些事件会同时触发,因此如果它们同时进入上述事件,它们将在lastRead被设置之前同时开始运行。 - Peter Jamsmenson
12
无法正常工作,因为触发的事件间隔很短:上次写入时间:636076274162565607 上次写入时间:636076274162655722 - Asheh
1
@Kjellski 在 Rx 中有一个名为 Throttle 的函数,在这种情况下非常有用。 - Krzysztof Skowronek
2
不像Asheh所解释的那样工作。这个会起作用:if (lastWriteTime.Ticks - lastRead.Ticks > 100000) - tala9999
显示剩余5条评论

27

这是我的解决方案,帮助我防止事件被触发两次:

watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size;

我在这里仅设置了NotifyFilter属性,包括文件名和大小。
watcher是我的FileSystemWatcher对象。希望这可以帮助你。


10
在记事本中,我创建了一个包含四个字符“abcd”的文件。然后我打开了一个新的记事本实例并输入了相同的四个字符。我选择文件|另存为,并选择了同样的文件。由于文件具有相同的四个字母,因此文件是相同的,文件大小和文件名不会改变,因此这不会触发任何操作。 - Rhyous
37
有可能进行真正的更改而不改变文件的大小,因此在这种情况下,该技术将失败。 - Lee Grissom
3
我猜这是一个相当普遍的情况,你知道任何有意义的更改都会修改文件大小(例如,我的情况是追加日志文件)。虽然任何使用此解决方案的人都应该意识到(并记录)这一假设,但这正是我所需要的。 - GrandOpener
2
@GrandOpener:这并不总是正确的。在我的情况下,我正在监视文件,其内容仅由一个字符组成,该字符为0或1。 - user4849927
这种方法对于小文件和文本文件可能会失败。但是对于我的二进制文件(例如图片,即使一个像素改变,整个文件大小也会改变),效果非常好。 - Gray Programmerz

11

这是我的方法:

// Consider having a List<String> named _changedFiles

private void OnChanged(object source, FileSystemEventArgs e)
{
    lock (_changedFiles)
    {
        if (_changedFiles.Contains(e.FullPath))
        {
            return;
        }
        _changedFiles.Add(e.FullPath);
    }

    // do your stuff

    System.Timers.Timer timer = new Timer(1000) { AutoReset = false };
    timer.Elapsed += (timerElapsedSender, timerElapsedArgs) =>
    {
        lock (_changedFiles)
        {
            _changedFiles.Remove(e.FullPath);
        }
    };
   timer.Start();
}
这是我在一个项目中解决文件作为邮件附件发送的问题时使用的解决方案。它可以轻松避免定时器间隔过小而导致事件被重复触发,但在我的情况下1000是可以接受的,因为我更喜欢错过一些更改而不是每秒发送超过1个消息导致邮箱被淹没。 至少在同时更改多个文件的情况下,它仍然可以正常工作。

我想到的另一个解决方案是用字典替换列表,将文件映射到其相应的MD5,这样您就不需要选择任意间隔,因为不必删除条目,而只需更新其值并在没有更改的情况下取消操作。 缺点是在文件被监视时会有一个内存中增长的字典,并且会占用越来越多的内存,但我曾经在某个地方读到过文件数量取决于FSW的内部缓冲区,因此也许不是那么关键。 我也不知道MD5计算时间会如何影响代码的性能,请小心处理。


你的解决方案对我非常有效。只是,你忘记将文件添加到_changedFiles列表中。代码的第一部分应该像这样:lock (_changedFiles) { if (_changedFiles.Contains(e.FullPath)) { return; } _changedFiles.Add(e.FullPath); // 添加这行! } // 做你的事情 - davidthegrey
你的解决方案不是线程安全的。_changedFiles被多个线程访问。修复的一种方法是使用ConcurrentDictionary代替List。另一种方法是将当前的Form分配给Timer.SynchronizingObject属性,以及FileSystemWatcher.SynchronizingObject属性。 - Theodor Zoulias
@TheodorZoulias,你用ConcurrentDicitonary让它正常工作了吗?因为对我来说,使用List很好,但是使用ConcurrentDicitonary会出问题。 - prinkpan
1
@PriyankPanchal 使用 ConcurrentDicitonary 有点复杂,因为它需要使用此类的专用并发 API。可能更容易的方法是使用上面评论中 davidthegrey 的建议,在访问 List<string> 之前添加 lock (_changedFiles)。否则,如果您只依赖于避免 非线程安全List<T> 类的并发变异的好运气,我也祝您永远好运。 - Theodor Zoulias

10
我已经创建了一个Git仓库,其中包括一个继承自FileSystemWatcher的类,用于仅在复制完成后触发事件。它会丢弃所有更改事件,只保留最后一个,并且只有当文件可供读取时才引发事件。
下载FileSystemSafeWatcher并将其添加到您的项目中。
然后像使用普通的FileSystemWatcher一样使用它,并监视事件何时被触发。
var fsw = new FileSystemSafeWatcher(file);
fsw.EnableRaisingEvents = true;
// Add event handlers here
fsw.Created += fsw_Created;

当目录上发生事件时,似乎会出现错误。我通过在打开文件之前包装一个目录检查来使其工作。 - Sam
尽管示例中有错别字,但这对我来说似乎是一个可行的解决方案。然而,在我的情况下,可能会在一秒钟内进行十几次更新,因此我不得不将_consolidationInterval大幅降低以避免错过任何更改。虽然10毫秒似乎很好,但如果我将_consolidationInterval设置为50毫秒,则仍会丢失约50%的更新。我仍然需要运行一些测试来找到最合适的值。 - user4849927
1
consolidationInterval 对我来说效果很好。我希望有人能够 fork 这个项目并将其打包成 NuGet 包。 - zumalifeguard
3
谢谢 :) 这解决了我的问题。希望用一个监视器创建和复制的事件能够正常工作,以解决这个问题。https://stackoverflow.com/questions/55015132/modified-event-of-filesystemwatcher-getting-triggered-multiple-times/ - techno
1
对我的应用程序有用。非常感谢。 - AlanC

9
我的情境是,在虚拟机中运行着一个Linux服务器。我在Windows主机上开发文件。当我在主机的文件夹中做出修改时,我希望所有的更改都能通过Ftp上传同步到虚拟服务器上。这样可以消除写入文件时产生的重复更改事件(标记包含文件的文件夹也被修改)。以下是我的解决方法:
private Hashtable fileWriteTime = new Hashtable();

private void fsw_sync_Changed(object source, FileSystemEventArgs e)
{
    string path = e.FullPath.ToString();
    string currentLastWriteTime = File.GetLastWriteTime( e.FullPath ).ToString();

    // if there is no path info stored yet
    // or stored path has different time of write then the one now is inspected
    if ( !fileWriteTime.ContainsKey(path) ||
         fileWriteTime[path].ToString() != currentLastWriteTime
    )
    {
        //then we do the main thing
        log( "A CHANGE has occured with " + path );

        //lastly we update the last write time in the hashtable
        fileWriteTime[path] = currentLastWriteTime;
    }
}

主要我创建了一个哈希表来存储文件写入时间信息。然后,如果哈希表有已修改的文件路径,并且其时间值与当前通知的文件更改相同,则我知道它是事件的重复,因此忽略它。


我假设你会定期清空哈希表。 - ThunderGr
这将精确到秒,但如果两个更改之间的时间间隔足够长以至于超过一秒钟,它将失败。此外,如果您想要更高的准确性,可以使用 ToString("o"),但要准备好面对更多的失败。 - Pragmateek
5
不要比较字符串,使用 DateTime.Equals() 方法。 - Phillip Kamikaze
不要这样做。它们并不相等。在我的当前项目中,它们相差约一毫秒。我使用 (newtime-oldtime).TotalMilliseconds < (任意阈值,通常为5ms)。 - Flynn1179

7

尝试使用以下代码:

class WatchPlotDirectory
{
    bool let = false;
    FileSystemWatcher watcher;
    string path = "C:/Users/jamie/OneDrive/Pictures/Screenshots";

    public WatchPlotDirectory()
    {
        watcher = new FileSystemWatcher();
        watcher.Path = path;
        watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
                               | NotifyFilters.FileName | NotifyFilters.DirectoryName;
        watcher.Filter = "*.*";
        watcher.Changed += new FileSystemEventHandler(OnChanged);
        watcher.Renamed += new RenamedEventHandler(OnRenamed);
        watcher.EnableRaisingEvents = true;
    }



    void OnChanged(object sender, FileSystemEventArgs e)
    {
        if (let==false) {
            string mgs = string.Format("File {0} | {1}",
                                       e.FullPath, e.ChangeType);
            Console.WriteLine("onchange: " + mgs);
            let = true;
        }

        else
        {
            let = false;
        }


    }

    void OnRenamed(object sender, RenamedEventArgs e)
    {
        string log = string.Format("{0} | Renamed from {1}",
                                   e.FullPath, e.OldName);
        Console.WriteLine("onrenamed: " + log);

    }

    public void setPath(string path)
    {
        this.path = path;
    }
}

5
什么信号量?我在这里只看到了一个布尔变量。此外,主要问题仍未解决:FileSystemEventHandler 仍然会触发多个事件。这段代码有什么效果吗?if (let==false) { ... } else { let = false; }?难以置信这个答案居然获得了赞,可能只是为了 StackOverflow 徽章而已。 - sɐunıɔןɐqɐp

5

我知道这是一个旧问题,但我遇到了同样的问题,以上解决方案都没有真正解决我的问题。我创建了一个字典,将文件名映射到LastWriteTime。所以如果文件不在字典中,将继续进行其他处理,否则检查最后修改时间是否与字典中的不同,如果不同,则运行代码。

    Dictionary<string, DateTime> dateTimeDictionary = new Dictionary<string, DateTime>(); 

        private void OnChanged(object source, FileSystemEventArgs e)
            {
                if (!dateTimeDictionary.ContainsKey(e.FullPath) || (dateTimeDictionary.ContainsKey(e.FullPath) && System.IO.File.GetLastWriteTime(e.FullPath) != dateTimeDictionary[e.FullPath]))
                {
                    dateTimeDictionary[e.FullPath] = System.IO.File.GetLastWriteTime(e.FullPath);

                    //your code here
                }
            }

这是一个可靠的解决方案,但缺少一行代码。在 your code here 部分,您应该添加或更新 dateTimeDictionary。 dateTimeDictionary[e.FullPath] = System.IO.File.GetLastWriteTime(e.FullPath); - DiamondDrake
1
对我而言并没有起作用。我的变更处理程序被调用了两次,而且文件时间戳在第二次有所不同。可能是因为这是一个很大的文件,而且第一次写入还在进行中。我发现使用计时器来折叠重复事件效果更好。 - michael

4

我花了很多时间使用FileSystemWatcher,这里的某些方法不起作用。我非常喜欢禁用事件的方法,但是如果有>1个文件被拖放,第二个文件大多数情况下会被错过。因此,我采用以下方法:

private void EventCallback(object sender, FileSystemEventArgs e)
{
    var fileName = e.FullPath;

    if (!File.Exists(fileName))
    {
        // We've dealt with the file, this is just supressing further events.
        return;
    }

    // File exists, so move it to a working directory. 
    File.Move(fileName, [working directory]);

    // Kick-off whatever processing is required.
}

1
谢谢,这个主题已经被讨论了很多次,也提出了许多解决方法。我认为你的方法是正确而简洁的。谢谢,Anthony Peiris - TonyP

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接