如何避免C#中FileSystemWatcher的错误?
目录中一次更改太多
我必须检测网络共享上的所有更改。InternalBufferSize 增加到 8192 * 128
如何避免C#中FileSystemWatcher的错误?
目录中一次更改太多
我必须检测网络共享上的所有更改。InternalBufferSize 增加到 8192 * 128
你应该做两件事:
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 通知。