由于选课关系,没有去听第一次软工课,接到individual project要求后,我大致浏览了下,感觉运用C# .NetFramework完成不是十分困难,但是,若要保证高效率,则需要在初期设计上下较大功夫。
考虑到问题的特点,我准备运用多线程手段实现,初步设想为Productor-Consumer模式,若干文件扫描线程(FileThreadScanner)作为生产者,将扫描到的文件通过并发队列(concurrent job queue)送入若干文件读取与分词计数线程(wordCounterThread),若干计数线程通过并发访问一个Dictionary类实现计数统计,计数规则根据需求(ordinary mode or extended mode)做相应调整
有了大致的设计框架,接下来则是进一步细化的工作,具体包括以下几点:
1. 文件扫描线程之间如何实现任务分配
文件扫描线程实现的实现原理不难,主要问题在于如何对递归形式的任务进行高效分配。这可以通过一个job queue实现,线程将扫描到的文件夹加入队列,然后从队列中取下一个待扫描的文件夹。但是,由于需要对job queue的dequeue()操作加锁,这样,考虑到问题的规模,加锁带来的线程同步调度的性能损耗反而可能会超过扫描文件本身。百般不思其解时,突然msdn到一个函数
public static IEnumerable<string> EnumerateFiles( string path, string searchPattern, SearchOption searchOption )
searchPattern=SearchOption.AllDirectories,即可实现递归搜索。
使用这个函数的好处是,函数返回的不是扫描结果,而是一个迭代器(enumerator),msdn上明确说明,该函数返回的迭代器在搜索未结束前就返回,这样,可以使得搜索操作与结果加入队列的操作同时进行,大大提高了程序的并发性。
IEnumerator<string> it = Directory.EnumerateFiles(fileDir, "*", SearchOption.AllDirectories).GetEnumerator(); while (it.MoveNext()) { string fileName = it.Current; if (isPropFile(fileName)) { //Console.WriteLine(fileName); lock (fileQueue) { fileQueue.Enqueue(fileName); } } }
这样,就不需要开多个文件扫描线程,在数据规模较小的情况下,使用一个即可。
2. 文件读取与分词线程如何实现高效
2.1 大文件是否需要并发读取
假设有一个100mb的文本文件,若使用streamreader一行行的读取、分词,肯定十分耗时。初步设想,当文件超过一个大小时,使用多个线程对其进行并发读取。但是,这又带来新的问题:该如何划分各个线程的读取范围。google了好久,都没有找到成熟的例子,而且我也不太熟悉C#的IO操作,因此该想法只好放弃
2.2 分词的高效做法
若采用一个字符一个字符判断的方式,效率肯定很低,因此冒着作业得零分的危险,自己划定了一个分隔符的范围,使用string.split(params char[] separator)函数对文本进行分割。
3. 文件读取与分词线程对Dictionary类的并发访问如何做到高效率同步
这是最重要的一点,首先,同步是必要的,如果两个分词线程同时读到一个单词,比如buaa,并同时向字典中提出添加请求,且如果之前字典中没有这个词,那么很有可能,字典类处理这两个请求时,都认为字典中没有这个词,这样就会造成一个buaa没有被计数,结果错误,因此,必须对字典的添加操作加锁。
if (Config.IsExtendMode) { tkey = delTailNum(word.ToLower()); } else { tkey = word.ToLower(); } lock (wordTable) { if (wordTable.ContainsKey(tkey)) { wordTable[tkey].IncAndTryReplace(word); } else { wordTable.Add(tkey, new WordEntry(word)); } }
虽然.net framework4中引入了concurrentdictionary类,但是我之前没有用过,对它的实现原理也不是很了解,保险起见,使用了上段所述的方案。
4. 结果的排序
优于dictionary类中存放的单词是无序的,因此需要在结果输出前进行一次排序,在这里,我尝试了一次linq排序dictionary元素的方法。
from pair in wordTable orderby pair.Value descending select pair
上面是整个的分析过程,也是此次作业中耗时仅次于编码的部分。由于我对C#的并发程序设计不是很熟悉,因此得出的这个模式可能不是最优的,但相信对于大规模的数据集,该方法会优于普通单线程的方法。
时间规划
问题分析 1h
文件扫描线程 40min
文件读取与分词线程 1h
字典类及并发处理 1h
Debug 0.5h
实际耗时
问题分析 1.5h
文件扫描线程 30min
文件读取与分词线程 1h
字典类及并发处理 1h15min
Debug 1.5h