前言
当我开始写这篇博客的时候有些不知所措。在课堂上我可以表现的很自信,他们不知道我做的什么,怎么做的,我怎么说都可以。可是当要写成博客,发布在CSDN上的时候我有点慌。可能没人会看到,但我还是怕,就好像一个小贼即将面对一群神探的审判那样。 因此我很认真的斟酌每一句话,生怕一开口就被别人听出Bug来。
读取
我费了一些周折看懂了别人的博客,读取出了USN日志的内容,又恰巧把自己在书上看的Task编程方法用上了 ,因为要并行读取多个磁盘。
后来我才发现TPL编程模型的应用是那么的广。
/// <summary>
/// 读取所有NTFS磁盘的USN日志
/// </summary>
/// <returns>查询结束的磁盘名</returns>
public IEnumerable<string> InitCollectionFromDisks()
{
_fileSource.Files = new BlockingCollection<FileStruct>(); ;
var tasks = new List<Task<string>>();
foreach(DriveInfo drive in _fileSource.Drives)
{
var task = QueryUsnDataAsync(drive.Name);
tasks.Add(task);
}
while (tasks.Count > 0)
{
var completeTask = Task.WhenAny(tasks).Result;
tasks.Remove(completeTask);
yield return completeTask.Result;
}
_fileSource.Files.CompleteAdding();
}
/// <summary>
/// 根据磁盘名,查询usn日志
/// </summary>
/// <param name="driveName"></param>
/// <returns></returns>
private Task<string> QueryUsnDataAsync(string driveName)
{
return Task.Factory.StartNew(() =>
{
UsnJournal usn = new UsnJournal(driveName);
usn.GetFiles(_fileSource.Files);
return driveName;
}, TaskCreationOptions.LongRunning);
}
//yield return是为了通知完成了查询的磁盘名称,以便在窗体的左下角展示出来。
我一开始把多任务读取出来的文件模型保存在了普通的List<>集合中, 因此在从List中查询的时候多次出现空异常,而且毫无规律,让人很头疼。后来才明白,List并非线程安全的集合,它的Add()方法分为两个步骤进行,多线程往其中添加元素,当前位置指针可能会移动两次,但只添加一个元素,因此会出现Null值的情况.
// Adds the given object to the end of this list. The size of the list is
// increased by one. If required, the capacity of the list is doubled
// before adding the new element.
//
public void Add(T item) {
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
因此我改用了并发集合BlockingCollection,它被称为线程安全的集合。我使用它对程序进行了修改。我并没有用到它什么特别的地方,它的内部实现了加锁解锁,适合用于多任务的管道应用,添加结束后需要调用CompleteAdding()方法,以通知读取器不用继续等待新元素的到来。
显示
文件读取出来后自然要想着怎么显示出来。Everything会把读取出的内容保存在自建的数据库中,并监视磁盘变化,实时更新。当我们打开它时,它表面上很轻松的显示出了所有的文件,而且还有文件的大小,时间等其他属性,不可思议。USN日志是不包含这些内容的,它是如何做到的呢?
我们先来看一下如何使用WinForm原生控件显示大量数据。
Control类的Invoke与BeginInvoke
当时我的电脑上有大约一百万个文件,我最先尝试直接给DataGirdView绑定数据,但是因为数据太多,这会导致界面假死。因此我想到在一个新的线程中逐步填充数据,而不是在主线程中。
new Thread(() =>
{
for (int i = 0; i < 10000000; i++)
{
listBox1.Items.Add(i.ToString());
}
}).Start();
这么写程序运行起来并没有问题,数据也是按照期望的那样逐步填充,界面也并没有出现假死的情况。但当我们启用调试的时候, listBox1.Items.Add(i.ToString()); 这一段代码就会出现问题,编译器会提示我们"不是从创建控件的线程访问无效"。这是出于某种考虑,WinForm不允许我们这么做。
后来在新浪博客找到了一篇文章,作者讲述了使用Control类的Invoke与BeginInvoke两个方法。
new Thread(()=>
{
progressBar1.Maximum =files.Count;
for (int i = 0; i < files.Count; i++)
{
listBox1.Invoke((Action)delegate ()
{
listBox1.Items.Add(files[i].Name);
});
progressBar1.Value = listBox1.Items.Count;
label5.Text = (Math.Round(((double)progressBar1.Value / progressBar1.Maximum), 2) * 100).ToString() + "%";
};
}).Start();
Invoke和BeginInvoke两个方法都是接受一个Delegate参数。我们使用delegate声明出来的委托对象也有两个同名的方法,在异步编程模式中这种函数命名十分常见。一般是以Begin/End方法对的形式出现。我们在做Socket通信时,可以启动一个异步操作,然后订阅给不同的事件。 Control类的Invoke和BeginInvoke也是同步和异步的关系吗?
Control.Invoke : 在拥有此控件的基础窗口句柄的线程上执行指定的委托
Control.BeginInvoke : 在创建控件的基础句柄所在的线程上异步执行指定的委托
来自: https://www.cnblogs.com/blosaa/archive/2013/05/30/3107381.html
事实上,这两个函数都是在UI线程上执行参数委托,也正因如此,我们才可以new一个线程,并在其中来操作UI控件。将listBox1.Items.Add(files[i].Name);这一步操作放在listBox1.Invoke中去执行,这会解决"不是从创建控件的线程访问无效"。的问题。
Delegate和delegate
你也可以对Control类的Invoke和BeginInvoke做更多的尝试。
让我们来关注另一个问题:Delegate是什么?delegate是我们声明一个委托类型的主要方式,而Delegate又有哪些作用呢?他们二者之间又有哪些关系呢?
System.Delegate 和 System.MulticastDelegate 类本身不是委托类型。 它们为所有特定委托类型提供基础。
语言设计过程要求不能声明派生自 Delegate 或 MulticastDelegate 的类。
使用的每个委托都派生自 MulticastDelegate。而Delegate又是MulticastDelegate的父类。
https://docs.microsoft.com/zh-cn/dotnet/csharp/delegate-class
那么我们怎么传入一个Delegate参数呢?我在网上找到了方法(Action)delegate(){}。匿名函数转化成委托,再用Action转换成强类型委托。这样编译器就可以将参数转化为Delegate类型了。为什么用Action?因为这是一个无返回值的匿名函数,关于Action就不再多做介绍了。当然还可以使用其他的强转方式,如MethodInvoker。
Abort函数
看似我们解决了UI显示的问题,填充开始后,ListBox的滑动条在快速的缩短,它代表着列表里的数据越来越多。如果我们在列表填充完成之前进行了查询操作,列表应刷新。但之前我们已经启动了一个线程进行填充列表,还记得吗?因此,我们需要先结束那个未完成的线程,清除列表内容,再启动一个新的线程。我对这段程序再次进行了调整。
Thread t;
private void Button1_Click(object sender, EventArgs e)
{
if (t!=null&&t.IsAlive)
t.Abort();
t = new Thread(() =>
{
progressBar1.Maximum = files.Count;
for (int i = 0; i < files.Count; i++)
{
listBox1.Invoke((MethodInvoker)delegate ()
{
listBox1.Items.Add(files[i].Name);
});
progressBar1.Value = listBox1.Items.Count;
label5.Text = (Math.Round(((double)progressBar1.Value / progressBar1.Maximum), 2) * 100).ToString() + "%";
};
}).Start();
}
但这么做效果并不好,因为Abort函数并不一定能结束目标线程。目标线程线程可以通过异常处理并调用ResetAbort方法来拒绝被终止。这听起来很神奇,因为Abort函数是通过给目标线程注入抛出异常的方法来实现终结线程的,该异常可能任何时刻发生摧毁线程。书中推荐使用CancellationToken来终止线程。我们可以从CancellationTokenSource中获取token对象,可以利用它编写程序,以轮询,抛出异常,注册回调方法等多种方式终结目标线程,具体操作这里不再介绍。
DataGridView的虚模式
但是我放弃了使用ListBox或是ListView的想法,因为我发现了DataGridView的虚模式的用法。它最终的使用效果和Everything是一模一样的!
当我们为dataGridView1开启了虚模式以后,只需要完成它的CellValueNeeded事件即可。
虚模式最直接的效果就是,列表只加载你要看到的那部分数据,当你拖动滑动条或是滑动滚轮时,它才会加载新的数据。你可以按照以下代码,使用e.RowIndex来决定当前行应该填充哪一条数据。
private void dataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
if (e.RowIndex == this.dataGridView1.RowCount - 1) return;
FileModel file;
if (refresh)
{
file = EnrichFileModel(Program.fileSource.TemporaryList[e.RowIndex]);
Program.fileSource.TemporaryFileModelList.Add(file);
}
else
{
file = Program.fileSource.TemporaryFileModelList[e.RowIndex];
}
switch (e.ColumnIndex)
{
case 0:
e.Value = file.FileName;
break;
case 1:
e.Value = file.FilePath;
break;
case 2:
e.Value = file.FileSize;
break;
case 3:
e.Value = file.CreateTime;
break;
case 4:
e.Value = file.WriteTime;
break;
}
}
TemporaryList保存的是从USN日志中查询出来的最基本的文件模型。要想得到文件的FileSize,CreateTime,WriteTime等信息,我使用到了一个自定义的函数EnrichFileModel用于获取一个内容更丰富的文件模型,将其添加到TemporaryFileModelList是为了给查询到数据排序时使用。
当我们查询某个文件时,dataGridView1应根据TemporaryFileList内的数据进行更新。我们需要先清除列表内原有的行,并为列表定义新的行数 。
dataGridView1.Rows.Clear();
dataGridView1.RowCount = Program.fileSource.TemporaryList.Count + 1;
RowCount定义完成后,列表便会触发CellValueNeeded事件,重新填充列表。
至此,我们已经实现了最重要的磁盘文件读取与显示功能。
如何实现Everything丰富的查询功能,以及制作自己的UI控件,以及为了参赛,加入的一些其他功能,下次再说。