C# 异步枚举文件夹
我正在尝试实现一个通用文件系统爬虫程序,例如,它能够枚举从给定根开始的所有子文件夹。我想使用async/await/Task范式来实现这一点 下面是到目前为止我的代码。它是有效的,但我怀疑它可以改进。特别是,带注释的C# 异步枚举文件夹,c#,asynchronous,filesystems,async-await,directory,C#,Asynchronous,Filesystems,Async Await,Directory,我正在尝试实现一个通用文件系统爬虫程序,例如,它能够枚举从给定根开始的所有子文件夹。我想使用async/await/Task范式来实现这一点 下面是到目前为止我的代码。它是有效的,但我怀疑它可以改进。特别是,带注释的Task.WaitAll会在深层目录树中导致不必要的等待,因为循环在每个树级别都暂停等待,而不是立即处理添加到folderQueue的新文件夹 不知何故,我希望在执行WaitAll时,将添加到folderQueue的新文件夹包括在Task.WaitAll()等待的任务列表中。这可能吗
Task.WaitAll
会在深层目录树中导致不必要的等待,因为循环在每个树级别都暂停等待,而不是立即处理添加到folderQueue
的新文件夹
不知何故,我希望在执行WaitAll
时,将添加到folderQueue
的新文件夹包括在Task.WaitAll()
等待的任务列表中。这可能吗
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
class FileSystemCrawlerSO
{
static void Main(string[] args)
{
FileSystemCrawlerSO crawler = new FileSystemCrawlerSO();
Stopwatch watch = new Stopwatch();
watch.Start();
crawler.CollectFolders(@"d:\www");
watch.Stop();
Console.WriteLine($"Collected {crawler.NumFolders:N0} folders in {watch.ElapsedMilliseconds} milliseconds.");
if (Debugger.IsAttached)
Console.ReadKey();
}
public int NumFolders { get; set; }
private readonly Queue<DirectoryInfo> folderQueue;
public FileSystemCrawlerSO()
{
folderQueue = new Queue<DirectoryInfo>();
}
public void CollectFolders(string path)
{
DirectoryInfo directoryInfo = new DirectoryInfo(path);
lock (folderQueue)
folderQueue.Enqueue(directoryInfo);
List<Task> tasks = new List<Task>();
do
{
tasks.Clear();
lock (folderQueue)
{
while (folderQueue.Any())
{
var folder = folderQueue.Dequeue();
Task task = Task.Run(() => CrawlFolder(folder));
tasks.Add(task);
}
}
if (tasks.Any())
{
Console.WriteLine($"Waiting for {tasks.Count} tasks...");
Task.WaitAll(tasks.ToArray()); //<== NOTE: THIS IS NOT OPTIMAL
}
} while (tasks.Any());
}
private void CrawlFolder(DirectoryInfo dir)
{
try
{
DirectoryInfo[] directoryInfos = dir.GetDirectories();
lock (folderQueue)
foreach (DirectoryInfo childInfo in directoryInfos)
folderQueue.Enqueue(childInfo);
// Do something with the current folder
// e.g. Console.WriteLine($"{dir.FullName}");
NumFolders++;
}
catch (Exception ex)
{
while (ex != null)
{
Console.WriteLine($"{ex.GetType()} {ex.Message}\n{ex.StackTrace}");
ex = ex.InnerException;
}
}
}
}
使用系统;
使用System.Collections.Generic;
使用系统诊断;
使用System.IO;
使用System.Linq;
使用System.Threading.Tasks;
类FileSystemCrawlerSO
{
静态void Main(字符串[]参数)
{
FileSystemCrawlerSO crawler=新FileSystemCrawlerSO();
秒表=新秒表();
watch.Start();
crawler.CollectFolders(@“d:\www”);
看,停;
WriteLine($“在{watch.elapsedmillesons}毫秒内收集了{crawler.NumFolders:N0}个文件夹。”);
if(Debugger.IsAttached)
Console.ReadKey();
}
公共int NumFolders{get;set;}
专用只读队列folderQueue;
公共文件系统crawlerso()
{
folderQueue=新队列();
}
公用文件夹(字符串路径)
{
DirectoryInfo DirectoryInfo=新的DirectoryInfo(路径);
锁(折叠队列)
folderQueue.Enqueue(directoryInfo);
列表任务=新列表();
做
{
任务。清除();
锁(折叠队列)
{
while(folderQueue.Any())
{
var folder=folderQueue.Dequeue();
Task Task=Task.Run(()=>CrawlFolder(文件夹));
任务。添加(任务);
}
}
if(tasks.Any())
{
WriteLine($“正在等待{tasks.Count}个任务…”);
Task.WaitAll(tasks.ToArray());//这是我的建议。我使用泛型并发类*
类,所以我自己不必处理锁(尽管这不会自动提高性能)
然后,我为每个文件夹启动一个任务,并在ConcurrentBag
中排队。启动第一个任务后,我总是等待包中的第一个任务,如果没有更多任务等待,我就完成了
public class FileSystemCrawlerSO
{
public int NumFolders { get; set; }
private readonly ConcurrentQueue<DirectoryInfo> folderQueue = new ConcurrentQueue<DirectoryInfo>();
private readonly ConcurrentBag<Task> tasks = new ConcurrentBag<Task>();
public void CollectFolders(string path)
{
DirectoryInfo directoryInfo = new DirectoryInfo(path);
tasks.Add(Task.Run(() => CrawlFolder(directoryInfo)));
Task taskToWaitFor;
while (tasks.TryTake(out taskToWaitFor))
taskToWaitFor.Wait();
}
private void CrawlFolder(DirectoryInfo dir)
{
try
{
DirectoryInfo[] directoryInfos = dir.GetDirectories();
foreach (DirectoryInfo childInfo in directoryInfos)
{
// here may be dragons using enumeration variable as closure!!
DirectoryInfo di = childInfo;
tasks.Add(Task.Run(() => CrawlFolder(di)));
}
// Do something with the current folder
// e.g. Console.WriteLine($"{dir.FullName}");
NumFolders++;
}
catch(Exception ex)
{
while (ex != null)
{
Console.WriteLine($"{ex.GetType()} {ex.Message}\n{ex.StackTrace}");
ex = ex.InnerException;
}
}
}
}
公共类文件系统crawlerso
{
公共int NumFolders{get;set;}
私有只读ConcurrentQueue folderQueue=新ConcurrentQueue();
私有只读ConcurrentBag任务=新建ConcurrentBag();
公用文件夹(字符串路径)
{
DirectoryInfo DirectoryInfo=新的DirectoryInfo(路径);
tasks.Add(Task.Run(()=>CrawlFolder(directoryInfo));
等待的任务;
while(tasks.TryTake(out taskToWaitFor))
taskToWaitFor.Wait();
}
专用文件夹(目录信息目录)
{
尝试
{
DirectoryInfo[]directoryInfos=dir.GetDirectories();
foreach(directoryInfos中的DirectoryInfo childInfo)
{
//这里可能有龙使用枚举变量作为闭包!!
DirectoryInfo di=childInfo;
tasks.Add(Task.Run(()=>CrawlFolder(di));
}
//对当前文件夹执行某些操作
//例如Console.WriteLine($“{dir.FullName}”);
NumFolders++;
}
捕获(例外情况除外)
{
while(ex!=null)
{
WriteLine($“{ex.GetType()}{ex.Message}\n{ex.StackTrace}”);
ex=ex.InnerException;
}
}
}
}
我还没有衡量这是否比你的解决方案快。但我认为(正如亚库布·马萨德)评论说,瓶颈将是IO系统本身,而不是您组织任务的方式。理论上,async/await应该可以在这里提供帮助。在实践中,没有这么多。这是因为Win32没有为目录函数(或某些文件函数,如打开文件)公开异步API
此外,通过使用多个线程(Task.Run
)并行化磁盘访问可能会适得其反,特别是对于传统(非SSD)磁盘。并行文件系统访问(与串行文件系统访问相反)往往会导致磁盘抖动,从而降低总体吞吐量
因此,在一般情况下,我建议只使用阻塞目录枚举方法。例如:
class FileSystemCrawlerSO
{
static void Main(string[] args)
{
var numFolders = 0;
Stopwatch watch = new Stopwatch();
watch.Start();
foreach (var dir in Directory.EnumerateDirectories(@"d:\www", "*", SearchOption.AllDirectories))
{
// Do something with the current folder
// e.g. Console.WriteLine($"{dir.FullName}");
++numFolders;
}
watch.Stop();
Console.WriteLine($"Collected {numFolders:N0} folders in {watch.ElapsedMilliseconds} milliseconds.");
if (Debugger.IsAttached)
Console.ReadKey();
}
}
使用这种简单方法的一个很好的副作用是,文件夹计数器变量(NumFolders
)上不再存在争用条件
对于控制台应用程序,这就是您需要做的所有事情。如果要将其放入UI应用程序中,并且您不想阻止UI线程,那么一个任务。运行就足够了。单独抓取和处理
尝试使用生产者-消费者模式。
这是一种在一个线程中抓取目录并在另一个线程中处理的方法
我猜您正在尝试加快文件夹枚举过程。您是否确实进行了测量以确保获得了一些性能?请注意,没有真正的异步API枚举文件夹或文件(至少是AFAIK)。实际上,您正在使用更多的线程来同步枚举文件夹,但这些线程将花费大部分时间等待同步IO完成。@YacoubMassad是的,这里的目标是更快。我确实衡量了我的解决方案相对于简单、连续、同步枚举的性能,这是确定的
public class Program
{
private readonly BlockingCollection<DirectoryInfo> collection = new BlockingCollection<DirectoryInfo>();
public void Run()
{
Task.Factory.StartNew(() => CollectFolders(@"d:\www"));
foreach (var dir in collection.GetConsumingEnumerable())
{
// Do something with the current folder
// e.g. Console.WriteLine($"{dir.FullName}");
}
}
public void CollectFolders(string path)
{
try
{
foreach (var dir in new DirectoryInfo(path).EnumerateDirectories("*", SearchOption.AllDirectories))
{
collection.Add(dir);
}
}
finally
{
collection.CompleteAdding();
}
}
}
Parallel.ForEach(collection.GetConsumingEnumerable(), dir =>
{
// Do something with the current folder
// e.g. Console.WriteLine($"{dir.FullName}");
});