避免目录中一次发生太多更改错误
本文关键字:一次 太多 错误 | 更新日期: 2023-09-27 18:04:44
如何避免 C# 中 FileSystemWatcher 的错误?
目录中一次更改过多
我必须检测网络共享上的所有更改。InternalBufferSize 增加到 8192 * 128<</p>
您应该做两件事:
- 将
InternalBufferSize
设置为支持的最大值 (65536(。您尝试将其设置为"8192 * 128"大于文档中列出的最大支持值,因此您可能根本没有增加缓冲区大小。 - 将事件从
FileSystemWatcher
排队到后台线程进行处理。
这是这里不太清楚的第二点,确实应该记录在 MSDN 上。在内部,FileSystemWatcher
将更改事件排队到您设置的上述大小的内部缓冲区中。但至关重要的是,只有在事件处理程序返回后,才会从该缓冲区中删除项。这意味着事件处理程序引入的每个开销周期都会增加缓冲区填充的可能性。你应该做的是尽快清除FileSystemWatcher
的有限队列,并将事件移动到你自己的无限队列中,以你可以处理的速度进行处理,或者如果你愿意,可以丢弃,但要有一些智能。
这基本上是我在代码中所做的。首先,我启动自己的调度程序线程:
Dispatcher changeDispatcher = null;
ManualResetEvent changeDispatcherStarted = new ManualResetEvent(false);
Action changeThreadHandler = () =>
{
changeDispatcher = Dispatcher.CurrentDispatcher;
changeDispatcherStarted.Set();
Dispatcher.Run();
};
new Thread(() => changeThreadHandler()) { IsBackground = true }.Start();
changeDispatcherStarted.WaitOne();
然后我创建观察程序。请注意正在设置的缓冲区大小。就我而言,我只观察目标目录中的更改,而不是子目录中的更改:
FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Path = path;
watcher.InternalBufferSize = 64 * 1024;
watcher.IncludeSubdirectories = false;
现在我附加了事件处理程序,但在这里我将它们调用到调度程序上,而不是在观察程序线程中同步运行它们。是的,事件将由调度程序按顺序处理:
watcher.Changed += (sender, e) => changeDispatcher.BeginInvoke(new Action(() => OnChanged(sender, e)));
watcher.Created += (sender, e) => changeDispatcher.BeginInvoke(new Action(() => OnCreated(sender, e)));
watcher.Deleted += (sender, e) => changeDispatcher.BeginInvoke(new Action(() => OnDeleted(sender, e)));
watcher.Renamed += (sender, e) => changeDispatcher.BeginInvoke(new Action(() => OnRenamed(sender, e)));
最后,在处理完FileSystemWatcher
之后(你这样做了,对吧?(,你需要关闭调度程序:
watcher.Dispose()
changeDispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);
仅此而已。我自己也遇到了这个问题,无论是在网络还是本地场景中。使用这种方法后,我无法再次生成此错误,即使尽快将空文件敲定到监视目录也是如此。如果您确实设法在这种情况下以某种方式耗尽缓冲区(我不确定是否可能,上游的 API 可能更慢(,这里还有进一步的优化空间。但是,只要您的调度程序超过"临界点",发件人发布事件的速度就不能比您调度事件的速度快,您就永远不会积压,因此永远不会破坏缓冲区。我相信这种方法会让你进入那个安全区域。
我想我可能已经找到了一种可以帮助显着改善缓冲区使用的模式。
此类的问题在于,在事件的委托完成运行之前,它无法释放用于保存该信息的内存。
对于我的一生,我不知道为什么最大 InternalBufferSize 设置为 64Kb,但有了这个想法,您将更有效地使用这个小缓冲区。
如果不是在委托中执行操作,而只是将它们排队并推迟后台工作进程的执行,则它使用的内存量将大大减小。
事实上,我不知道为什么类本身一开始就没有实现这样的东西。
下面的代码只是这个想法的一个示例代码段,不应该在生产环境中使用,但它大大增加了我可以复制和跟踪的文件数量。
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.IO;
using System.Threading;
using NUnit.Framework;
namespace Soundnet.Synchronisation.FileSystemWatcherTests
{
/// <summary>
///
/// </summary>
[TestFixture]
public class Tests
{
static readonly ConcurrentQueue<Change> ChangesQueue = new ConcurrentQueue<Change>();
const string Destination = @"c:'Destination";
const string Source = @"c:'Source";
/// <summary>
/// Tests this instance.
/// </summary>
[Test]
public void Test()
{
var changesBackgroundWorker = new BackgroundWorker();
changesBackgroundWorker.DoWork += ChangesBackgroundWorkerOnDoWork;
changesBackgroundWorker.RunWorkerAsync();
var fileSystemWatcher = new FileSystemWatcher
{
Path = Source,
EnableRaisingEvents = true,
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.LastAccess | NotifyFilters.CreationTime | NotifyFilters.FileName | NotifyFilters.DirectoryName,
InternalBufferSize = 65536
};
fileSystemWatcher.Created += FileSystemWatcherOnCreated;
fileSystemWatcher.Deleted += FileSystemWatcherOnDeleted;
fileSystemWatcher.Renamed += FileSystemWatcherOnRenamed;
fileSystemWatcher.Error += FileSystemWatcherOnError;
while (true)
Thread.Sleep(1000000);
}
/// <summary>
/// Changeses the background worker configuration document work.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="doWorkEventArgs">The <see cref="DoWorkEventArgs"/> instance containing the event data.</param>
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
private static void ChangesBackgroundWorkerOnDoWork(object sender, DoWorkEventArgs doWorkEventArgs)
{
while (true)
{
Change change;
if (ChangesQueue.TryDequeue(out change))
{
var backgroundWorker = new BackgroundWorker();
switch (change.ChangeType)
{
case WatcherChangeTypes.Created:
backgroundWorker.DoWork += (o, args) =>
{
var fileSystemType = GetFileSystemType(change.FullPath);
var newItem = Path.Combine(Destination, change.Name);
while (true)
{
try
{
switch (fileSystemType)
{
case FileSystemType.File:
File.Copy(change.FullPath, newItem, true);
break;
case FileSystemType.Directory:
var directorySecurity =
Directory.GetAccessControl(change.FullPath);
Directory.CreateDirectory(newItem, directorySecurity);
break;
case FileSystemType.NotExistant:
break;
}
return;
}
catch (IOException exception)
{
Thread.Sleep(100);
Console.WriteLine(exception.Message);
}
}
};
break;
case WatcherChangeTypes.Deleted:
backgroundWorker.DoWork += (o, args) =>
{
var itemToDelete = Path.Combine(Destination, change.Name);
var fileSystemType = GetFileSystemType(itemToDelete);
switch (fileSystemType)
{
case FileSystemType.File:
File.Delete(itemToDelete);
break;
case FileSystemType.Directory:
Directory.Delete(itemToDelete, true);
break;
}
};
break;
case WatcherChangeTypes.Changed:
backgroundWorker.DoWork += (o, args) =>
{
var fileSystemType = GetFileSystemType(change.FullPath);
var newItem = Path.Combine(Destination, change.Name);
switch (fileSystemType)
{
case FileSystemType.File:
File.Copy(change.FullPath, newItem, true);
break;
}
};
break;
case WatcherChangeTypes.Renamed:
backgroundWorker.DoWork += (o, args) =>
{
var fileSystemType = GetFileSystemType(change.FullPath);
var oldItem = Path.Combine(Destination, change.OldName);
var newItem = Path.Combine(Destination, change.Name);
switch (fileSystemType)
{
case FileSystemType.File:
if (File.Exists(oldItem))
File.Move(oldItem, newItem);
break;
case FileSystemType.Directory:
if (Directory.Exists(oldItem))
Directory.Move(oldItem, newItem);
break;
}
};
break;
case WatcherChangeTypes.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
backgroundWorker.RunWorkerAsync();
}
}
}
/// <summary>
/// Files the system watcher configuration created.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="fileSystemEventArgs">The <see cref="FileSystemEventArgs"/> instance containing the event data.</param>
private static void FileSystemWatcherOnCreated(object sender, FileSystemEventArgs fileSystemEventArgs)
{
ChangesQueue.Enqueue(new Change
{
ChangeType = WatcherChangeTypes.Created,
FullPath = fileSystemEventArgs.FullPath,
Name = fileSystemEventArgs.Name
});
}
/// <summary>
/// Files the system watcher configuration deleted.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="fileSystemEventArgs">The <see cref="FileSystemEventArgs"/> instance containing the event data.</param>
private static void FileSystemWatcherOnDeleted(object sender, FileSystemEventArgs fileSystemEventArgs)
{
ChangesQueue.Enqueue(new Change
{
ChangeType = WatcherChangeTypes.Deleted,
FullPath = fileSystemEventArgs.FullPath,
Name = fileSystemEventArgs.Name
});
}
/// <summary>
/// Files the system watcher configuration error.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="errorEventArgs">The <see cref="ErrorEventArgs"/> instance containing the event data.</param>
private static void FileSystemWatcherOnError(object sender, ErrorEventArgs errorEventArgs)
{
var exception = errorEventArgs.GetException();
Console.WriteLine(exception.Message);
}
/// <summary>
/// Files the system watcher configuration renamed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="fileSystemEventArgs">The <see cref="RenamedEventArgs"/> instance containing the event data.</param>
private static void FileSystemWatcherOnRenamed(object sender, RenamedEventArgs fileSystemEventArgs)
{
ChangesQueue.Enqueue(new Change
{
ChangeType = WatcherChangeTypes.Renamed,
FullPath = fileSystemEventArgs.FullPath,
Name = fileSystemEventArgs.Name,
OldFullPath = fileSystemEventArgs.OldFullPath,
OldName = fileSystemEventArgs.OldName
});
}
/// <summary>
/// Gets the type of the file system.
/// </summary>
/// <param name="fullPath">The full path.</param>
/// <returns></returns>
private static FileSystemType GetFileSystemType(string fullPath)
{
if (Directory.Exists(fullPath))
return FileSystemType.Directory;
if (File.Exists(fullPath))
return FileSystemType.File;
return FileSystemType.NotExistant;
}
}
/// <summary>
/// Type of file system object
/// </summary>
internal enum FileSystemType
{
/// <summary>
/// The file
/// </summary>
File,
/// <summary>
/// The directory
/// </summary>
Directory,
/// <summary>
/// The not existant
/// </summary>
NotExistant
}
/// <summary>
/// Change information
/// </summary>
public class Change
{
/// <summary>
/// Gets or sets the type of the change.
/// </summary>
/// <value>
/// The type of the change.
/// </value>
public WatcherChangeTypes ChangeType { get; set; }
/// <summary>
/// Gets or sets the full path.
/// </summary>
/// <value>
/// The full path.
/// </value>
public string FullPath { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the old full path.
/// </summary>
/// <value>
/// The old full path.
/// </value>
public string OldFullPath { get; set; }
/// <summary>
/// Gets or sets the old name.
/// </summary>
/// <value>
/// The old name.
/// </value>
public string OldName { get; set; }
}
}
来自 MSDN
;
Windows 操作系统会通知您的组件文件更改 在
FileSystemWatcher
创建的缓冲区中。如果有很多 在短时间内发生变化,缓冲区可能会溢出。这会导致 组件会丢失对目录中更改的跟踪,它只会 提供一揽子通知。增加缓冲区的大小InternalBufferSize
属性很贵,因为它来自 无法换出到磁盘的非分页内存,因此请保留 缓冲区小而大,不会错过任何文件更改事件。 若要避免缓冲区溢出,请使用NotifyFilter
和IncludeSubdirectories
属性,以便过滤掉不需要的更改 通知。
我的经验是,FileSystemWatcher 并不总是最可靠的使用方式。您可以指定筛选器以缩小正在监视的文件的范围 (NotifyFilter(,或增加缓冲区大小。
但根据您的要求,您可能还希望以另一种方式执行此操作,例如每 x 秒轮询一次以获取文件列表。但是,您可能需要告诉我们有关您的业务案例的更多信息。
如果您增加缓冲区大小,则应修复此问题,但这不是一个实用的解决方案。因为为了确保它始终记录所有内容,您需要使缓冲区变得巨大。这将极大地影响性能。我认为性能问题可以通过实现多线程来解决。
SHChangeNotifyRegister 可用于获取 shell 通知。