多线程文件/文件夹查找器
文件查找速度很快,尤其是在拥有多个物理驱动器的情况下;版本 2.1.0.17。
引言
为什么要对另一个搜索程序感兴趣?
- 如果您需要搜索多个物理磁盘驱动器,文件查找会更快。驱动器是并行搜索的,搜索时间只相当于最慢驱动器的时间。
- 文件查找允许您搜索文件、文件夹或两者。
- 您可以在一次搜索中使用多个掩码。
- 您可以使用包含和排除过滤规则的组合来搜索驱动器和/或文件夹的子集。
- 驱动器可通过驱动器号或卷标选择;对于可移动驱动器非常方便。
- 您可以同时运行多个文件查找实例。
- 文件查找不需要一个持续运行的后台任务来索引文件。
- 时间范围搜索精确到分钟,您可以按创建、修改或上次访问日期进行检查。
- 提供两种数据视图:文件和文件夹名称的列表视图,以及包含更详细信息的网格视图。网格视图可自定义,仅显示您感兴趣的字段。列表视图可保存到文本文件,而网格视图可保存到制表符分隔值文件。
- 可以直接从查看表单中启动或删除文件和文件夹。
- 在多显示器系统中,文件查找用户友好。
- 您可以通过选择在搜索过程中包含或排除哪些驱动器和路径来定制驱动器搜索。这些过滤列表可以保存以便将来重用。
- 文件查找具有外部接口,因此其他程序可以利用文件查找的快速并行搜索功能来定位感兴趣的文件和/或文件夹。文件查找通过命名管道返回文件和文件夹的名称。
安装和使用
下载 FileFindBinaries.zip 文件,打开它,然后选择 .NET 3.5 或 4.0 版本目录。将可执行文件复制到您选择的文件夹。如果您安装了 .NET 4.0,4.0 版本会快得多。FileFindBatchInterface 程序是可选的,将在下面的外部接口主题中进行解释。
您可能想创建一个桌面快捷方式来启动 FileFind.exe。目前,只需双击程序即可执行它。
默认情况下,文件查找将搜索所有磁盘驱动器上的所有文件夹。许多系统文件夹包含没有用户文件的文件,因此您首先要做的就是创建一个过滤列表来排除这些文件夹。通过按“Filters”按钮、“New”按钮和“Copy defaults”按钮来创建过滤列表。您应该会看到一个与下图类似的表单
输入过滤列表的名称,然后按“Save”按钮,再按“OK”按钮。现在,选择包含您刚创建的过滤列表名称的行,然后按“OK”按钮。您将回到初始表单,并应看到一条“Using filter list”消息,其中包含您的过滤列表名称。
有关创建和编辑过滤列表的更详细说明,请在“Edit Filter List”表单上选择“Help”。
输入搜索掩码。掩码由文件或文件夹名称中有效的任何字符加上通配符(* 或 ?)组成。星号匹配零个或多个字符,问号匹配任何单个字符。所有匹配都不区分大小写。多个掩码用分号 (;) 分隔。例如,*.jpg;xyz?.pdf 将搜索文件扩展名为 .jpg 的文件以及所有名称为四个字符且以 xyz 开头的 PDF 文件。
通过在“Search for”文本区域中按 Enter 键,或按“Start search”按钮开始搜索。随意探索界面。除非您按“Delete”按钮或在选择了网格视图中的一个或多个行后按键,否则您不会更改系统上的任何文件。
有一种简单的方法可以看到文件查找在您的系统上有多快。首先,运行一次扫描以加载系统缓存。重新运行扫描,并记下表单底部的经过时间。转到菜单中的 Options->Sequential search selection 并勾选它。重新运行扫描,并将经过时间与之前的搜索时间进行比较。如果您正在搜索多个物理驱动器,文件查找在并行模式下的搜索时间将是搜索具有最多文件或响应时间最慢的驱动器所花费的时间。
您可以同时运行多个文件查找实例,但同时运行多个实例并更新过滤列表是不行的。
过滤列表可以被多个实例共享,但每个用户都有自己的过滤列表集。过滤列表和各种选项保存在隔离存储中。隔离存储对用户和程序集是唯一的。如果您决定删除或移动程序,您应该通过执行程序并从主菜单中选择“Options”→“Remove saved options”,然后退出程序来删除隔离存储。
如果您要将可执行程序移动到另一个文件夹,您可能希望在移动前导出过滤列表,并在文件查找程序移动后导入它们。导出和导入命令在“Filter List Selection and Maintenance”表单的“File”菜单项下可用,该表单在您按“Filters”按钮时显示。导出和导入是在用户之间传输一个或多个过滤列表的简便方法。
如果您拥有文件查找的先前版本并想保存其中的排除列表,您需要将新的 FileFind.exe 复制到与旧 FileFind.exe 相同的目录中。新的 FileFind.exe 将通过复制旧排除列表的内容来自动创建一个新的过滤列表。过滤列表的名称将是“Prior Excludes”。
详细的使用说明可在“Help”菜单下找到。帮助说明已针对各种表单进行了定制。
代码
如果您只想使用查找应用程序,您可以停止阅读。本文的其余部分将介绍应用程序的架构、使用的一些编码技术,以及遇到的有趣问题及其解决方案。
该项目是使用 Visual C# 2008 Express Edition 开发的,目标框架为 .NET 2.0。文件查找 3.0 版本迫使我迁移到 .NET 3.5 以支持 IsolatedStorageFileStream
。我在 XP、Vista 和 Windows 7 上测试了代码。如果您安装了 .NET 4.0,我强烈建议您重新编译程序以使用 4.0 框架。速度会明显提高。
架构
逻辑/物理驱动器解析
目录搜索是 IO 密集型的,这使得它们成为多线程架构的理想选择,即使在单核机器上也是如此。文件查找的设计利用了这一点,为每个物理磁盘启动一个搜索线程。
一个物理磁盘可能有一个以上的分区,所以我必须将逻辑分区与物理磁盘进行匹配。这个逻辑已合并到 ResolveLogicalPhysicalDisks
类中。您传入一个驱动器字母的 ArrayList
,每个条目一个字母,格式如下:
- C:\
- D:\
- F:\
并返回一个已解析驱动器的 ArrayList
- C:\;F:\
- D:\
发现 C 和 F 分区位于同一个物理驱动器上。这两个分区将被分配给同一个搜索线程,以避免磁盘磁头争用。驱动器上可能还有其他分区,但用户未选择这些分区进行搜索。
ResolveLogicalPhysicalDisks
类提供同步和异步方法调用。解析过程使用 Windows Management Instrumentation (WMI) 调用来执行工作。WMI 速度不是很快,但该技术的一个优点是任何节能型处于睡眠模式的驱动器都会唤醒并准备好进行搜索。驱动器解析在程序启动时或通过直接选择或新的过滤列表更改磁盘选择时触发。这可能会在程序初始化期间导致问题,因为当您选择新的过滤列表时,解析可能正在运行。我在 Form1.cs 中使用 ManualResetEvent
来防止重新进入 ResolveLogicalPhysicalDisks
类。我在构建过滤规则时也使用解析过程来选择驱动器或路径。构建过滤规则没有重新进入问题。
如果您需要在其他程序中使用 ResolveLogicalPhysicalDisks
类,您可能需要手动添加对 System.Management
的引用。
数据持久化和表单之间的数据通信
所有持久化数据都可以通过 ConfigInfo
类访问。ConfigInfo
在程序初始化期间从先前的文件查找执行加载数据,并在终止期间保存所有持久化数据。项目属性中设置的调试常量用于确定数据是保存在隔离存储还是 XML 文件中。通过能够查看和编辑实际的 XML 文件而不是保存在隔离存储中的数据,简化了调试。以 XML 格式存储数据简化了新功能的添加。
ConfigInfo
类还包含多个表单所需的数据。例如,表单字体信息。菜单选项允许用户更改使用的字体。字体的特征保存在 ConfigInfo
类中,并在表单加载过程中被每个表单访问。网格视图信息、过滤列表集合和菜单选项也保存在 ConfigInfo
类中。
用户界面
主线程 Form1.cs 显示初始表单并执行与搜索过程相关的所有用户界面功能。列表视图和网格视图共享显示表单上的同一区域。您通过按“Grid view”或“List view”按钮来选择要使用的视图(文本根据当前选择的视图而变化)。
启动表单(由 Form1.cs 控制)上显示了最常用的功能。此表单包含 listBox1
和 gridView
控件,分别显示列表视图和网格视图。这两个视图显示不同的信息,但占用表单上的同一空间。Form1
设计表单显示了一个比 listBox1
控件小的网格视图控件。gridView
控件位于 listBox1
控件的上方。在 Form1
加载时,gridView
控件的边界被设置为等于 listBox1
控件的边界。用户可以随时决定显示哪个控件。
表单上的按钮会根据需要启用和禁用。按钮上的文本也会根据需要更改。例如,“Start search”按钮在输入掩码之前是禁用的。“Start search”在搜索开始时变为“Stop search”。您可以通过按“Stop search”按钮来终止当前搜索,此时您可能会短暂看到“Stopping search”按钮。按“Stopping search”按钮将立即停止当前搜索。
很容易用来自许多扫描线程的消息淹没 Form1
。随着消息到达率的增加,表单的响应能力会下降。因此,我测量了每间隔的消息数量,并根据高费率持续时间的长短来改变消息的显示方式。
跟踪变量和常量是
private int msgsPerInterval = 0;
private const int MaxRate = 300; //maximum number of messages per MsgInterval
private const int PauseValue = 20; //milliseconds to pause busy threads
private const long MsgInterval = 500; //milliseconds between message intervals
private enum InUpdate { InUpdateNo, InUpdateSpeed, InUpdatePausing }
private InUpdate inUpdate = InUpdate.InUpdateNo;
在处理了 MaxRate
条消息之前,不进行任何监视。如果在 MsgInterval
期间收到了超过 MaxRate
的消息,则会执行以下两个操作之一。首先,通过 listBox1.BeginUpdate();
table.BeginLoadData();
命令停止表单的可见更新,程序进入 InUpdateSpeed
处理模式,持续一段计算好的时间。在该时间段结束时,再次检查消息到达率。如果费率是 MaxRate
的两倍以上,则要求扫描任务短暂暂停(请参见下面的 SendFileInfo
和 SendDirectoryInfo
)。所有监视都关闭,然后监视周期重新开始。
用户界面的其余部分由表单和对话框组成。除了 Form1
之外,所有表单和对话框都有一个由调用者设置的 FormLocation
属性。新表单将尝试显示在调用表单所在的同一监视器上。如果多个文件查找正在执行,这将特别有用。
要完成此操作的调用方代码很简单,如下所示
using (AboutBox aboutDialog = new AboutBox())
{
aboutDialog.FormLocation = this.Location;
aboutDialog.ShowDialog();
}
新表单中的属性定义如下
private Point location = new Point();
private bool locationSet = false;
public Point FormLocation
{
get { return location; }
set
{
location = value;
location.X += 10; //add a small offset value
location.Y += 10;
locationSet = true;
}
}
线程
有两种类型的线程:用户界面线程和扫描线程。
文件查找只有一个用户界面 (UI) 线程,但可能有许多扫描线程。UI 线程拥有启动表单并处理最常见的用户界面功能。UI 线程还控制扫描线程。
文件查找为每个正在搜索的物理磁盘创建一个线程,由 ResolveLogicalPhysicalDisks
类识别。线程在 List<Thread>
类 searchThreads
中进行跟踪。创建线程时,将扫描线程添加到 searchThreads
,线程结束时将其移除。当 searchThreads.Count
为零时,搜索完成。线程的名称基于线程要搜索的驱动器的名称。
当用户按下“Start search”按钮时,UI 中的 StartSearching()
方法会被调用。StartSearching()
在构建和启动 DiskScan
线程之前,会验证所有必需条件是否已满足(驱动器解析必须完成,至少选择了一个驱动器,并提供了至少一个文件掩码)。
为每个要搜索的已解析驱动器创建一个 DiskScan
类的实例。类构造函数的参数列表包含一些信息字段和一组委托,以便在 UI 和扫描线程之间传递信息。DiskScan
中的构造函数定义如下
public DiskScan(
ContainerControl form, //the user interface
Delegate DsplyMsg, //send text messages
Delegate SendDir, //send directory information
Delegate SendFile, //send file information
ManualResetEvent terminateRequest, //receive a termination request
Delegate ThreadStopping, //send an end of thread message
DateTimeInfo selectedDateTime //provides date/time range information
)
UI 中匹配的字段定义是
private delegate void DelegateSendMsg(String s);
private DelegateSendMsg m_DelegateSendMsg;
private delegate void DelegateThreadStopped(string s);
private DelegateThreadStopped m_DelegateThreadStopped;
private delegate int SendFileInfo(FileInfo fi, string volumeId);
private SendFileInfo m_SendFileInfo;
private delegate int SendDirectoryInfo(DirectoryInfo dir, string volumeId);
private SendDirectoryInfo m_SendDirectoryInfo;
SendFileInfo
和 SendDirectoryInfo
委托有两个用途。DiskScan
使用这些委托将信息发送到 UI 以供显示,UI 返回 DiskScan
线程在发送更多信息之前应暂停的毫秒数。仅当 UI 收到过高的消息速率时,暂停间隔才为非零。
构造函数的参数列表变得越来越庞大,因此通过在将线程添加到 searchThreads
列表并启动线程之前使用类属性,将一些信息提供给 DiskScan
线程。UI 中的以下代码创建、跟踪和启动线程。
foreach (String resolvedDrive in resolvedDrives)
{
// create worker thread instance
DiskScan diskScan = new DiskScan(this,
m_DelegateSendMsg, m_SendDirectoryInfo, m_SendFileInfo,
pleaseTerminate, m_DelegateThreadStopped, dateTimeInfo);
diskScan.SearchingDrives = resolvedDrive;
if (filesOnlyRadioButton.Checked)
diskScan.SearchForFiles = true;
else if (foldersOnlyRadioButton.Checked)
diskScan.SearchForFolders = true;
else if (filesAndFoldersRadioButton.Checked)
diskScan.SearchForBoth = true;
diskScan.IFindFiles = fileMasks;
Thread searchThread = new Thread(new ThreadStart(diskScan.Run));
searchThread.Name = resolvedDrive;
searchThreads.Add(searchThread);
searchThread.Start();
} //ends foreach(String resolvedDrive in...
线程终止取决于唯一的线程名称,因此我使用线程正在搜索的驱动器的名称作为线程名。UI 将搜索 searchThreads
列表以查找线程名并将其移除。当 searchThreads
为空时,搜索完成。diskScan.Run()
方法被包装在 try
块中,以确保在扫描线程终止之前始终发出以下命令
try
{
m_form.Invoke(ThreadTerminating,
new Object[] { Thread.CurrentThread.Name });
}
catch {}
线程终止在 UI 中异步运行,因此所有对 searchThreads
的引用和操作都必须受到 threadobj
锁的保护。
有很多原因可以提前终止搜索——您输错了掩码,您注意到您使用了错误的过滤列表,您已经看到了要搜索的文件,等等。UI 允许用户通过“Stop searching”按钮提前终止搜索。当按下“Stop searching”按钮时,ManualResetEvent pleaseTerminate
会被设置。DiskScan
线程被赋予 pleaseTerminate
事件的引用,并且 DiskScan
会定期检查事件是否已被信号化。如果已信号化,DiskScan
将终止。当窗体关闭或外部接口关闭命名管道时,也会设置 pleaseTerminate
事件。
多监视器支持
我通常使用两个监视器。Windows 默认的表单放置操作在主监视器上效果很好,但在文件查找表单位于次监视器上时效果很差;其他表单会显示在主监视器上。因此,我在弹出表单中添加了一个 FormLocation
属性,以便将弹出表单放置在当前表单位置附近。
调用方的代码很简单,如下所示
using (SelectFilterListToCopy copyFilterList = new SelectFilterListToCopy())
{
copyFilterList.FormLocation = this.Location;
copyFilterList.ShowDialog();
. . .
弹出表单的属性定义如下
private Point formLocation = new Point(0, 0);
private bool formLocationSet = false;
public Point FormLocation
{
get { return formLocation; }
set { formLocation = value; formLocationSet = true; }
}
外部接口
文件查找具有快速搜索多个磁盘驱动器的能力。其他程序也可能需要搜索多个磁盘驱动器。我可以创建一个其他程序将使用的类,但搜索的异步性质和搜索结果的呈现将使类非常复杂。我决定创建一个命令行风格的接口来提供搜索标准,并使用命名管道来返回搜索结果。参数如下
- FilterList: FilterList 提供要使用的过滤列表的名称。过滤列表必须已存在。通过执行 FileFind.exe 并指向包含 FileFind.exe 的目录来创建过滤列表,然后在启动 FileFind 时。
- PipeName: PipeName 提供要使用的管道名称。可以在另一台系统上创建管道,但 FileFind 目前没有登录协议。我通过在两台计算机上以同一用户身份登录来进行跨系统测试。
- Masks: Masks 与文件查找主表单上的“Search for”字段相同。
- FilesFolders: 这决定了您是在搜索仅文件、仅文件夹还是两者。FilesFolders 的有效值为
文件
文件夹
两者
- Visible: 允许您显示 FileFind 表单(通常不可见),有助于调试您的应用程序。有效值为
是
否
关键字不区分大小写,但分号和值之间不允许有空格。如果需要在值中包含空格,请在整个参数周围加上双引号。
- Masks: *.jpeg 不合法,分号后有空格
- Masks:*.jpeg 合法
- FilterList: My defaults 不合法,参数值中嵌入了空格
- "-FilterList:My defaults" 合法
提供了一个 C# 示例程序 FileFindBatchInterface,实现了上述接口调用。该示例允许您运行同步或异步命名管道。在运行 FileFindBatchInterface.exe 之前,通过执行文件查找来定义和保存您想使用的任何需要定义的过滤列表。如果您将 FileFind.exe 和 FileFindBatchInterface.exe 放在同一个目录中,则不需要路径。
示例程序的线程结构与文件查找结构非常相似:一个 List<Thread>
用于跟踪运行中的任务,一个 ManualResetEvent
用于请求线程终止。然而,PipeServer
线程在等待管道连接时无法轮询终止信号。下面的代码对异步管道有效
WaitHandle[] waitHandles = new WaitHandle[]
{
new AutoResetEvent(false), //requested termination
new AutoResetEvent(false) //pipe I/O completion
};
waitHandles[0] = pleaseTerminate;
using (NamedPipeServerStream serverPipe = new
NamedPipeServerStream(pipeName, PipeDirection.In, 1,
PipeTransmissionMode.Message, PipeOptions.Asynchronous))
{
// wait for a client...
UpdateStatus("Waiting for asynchronous connection");
IAsyncResult waitConnection;
try
{
waitConnection = serverPipe.BeginWaitForConnection(null, null);
}
catch (Exception ex)
{
SendMsg("Server BeginWaitForConnection failed\n" + ex.Message);
return;
}
waitHandles[1] = waitConnection.AsyncWaitHandle;
WaitHandle.WaitAny(waitHandles);
if (pleaseTerminate.WaitOne(0, true))
return;
//. . . processing continues
WaitHandle.WaitAny(waitHandles)
将一直阻塞,直到 pleaseTerminate
事件发生或管道连接。管道服务器检查哪个事件发生了并采取相应的操作。
对于同步管道来说,情况会更复杂一些,因为没有可用的连接事件。管道服务器被阻塞在 WaitForConnection
调用上,直到管道连接发生才会看到 pleaseTerminate
事件。因此,UI 线程必须通过打开和关闭管道来提供连接。
using (NamedPipeClientStream pipe =
new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.None))
{
pipe.Connect(3000);
特点
我想改进对网络磁盘的搜索,并研究 ListView
控件的虚拟模式。
我在 Windows XP、Vista 和 Windows 7 上测试了此代码。该代码已使用两年多,看起来很稳定。
致谢
- 我要感谢 reinux 发布的文章“Converting Wildcards to Regexes”,网址为 https://codeproject.org.cn/KB/recipes/wildcardtoregex.aspx。他的例程使我能够快速地将通配符转换为正则表达式,从而可以轻松地将掩码与名称匹配。
- 我使用 Simon Morier 代码的修改版本来将逻辑分区与物理磁盘关联起来。我在 https://codeproject.org.cn/KB/system/usbeject.aspx 上找到了 Simon 的文章。
- 我还要感谢 Dave Midgley 关于重解析点的优秀文章,网址为 https://codeproject.org.cn/KB/vista/ReparsePointID.aspx。
- 我创建了 Carl Daniel 的“File system enumerator using lazy matching”代码的修改版本,该代码可在:FileSystemEnumerator.aspx 找到。
- 我没有创建手电筒图标。它来自网络上的某个免费图标网站。我感谢创造它并使其公开提供的人。
- 我还要感谢赞助 CodeProject.com 的各位以及众多贡献者。我显然经常使用这个网站!
历史
- 版本 2.1.0.17 - 新基线。
- 版本 3.0.0.0 - 添加了包含/排除过滤列表。
- 添加了外部接口。