C# 异步枚举文件夹

C# 异步枚举文件夹,c#,asynchronous,filesystems,async-await,directory,C#,Asynchronous,Filesystems,Async Await,Directory,我正在尝试实现一个通用文件系统爬虫程序,例如,它能够枚举从给定根开始的所有子文件夹。我想使用async/await/Task范式来实现这一点 下面是到目前为止我的代码。它是有效的,但我怀疑它可以改进。特别是,带注释的Task.WaitAll会在深层目录树中导致不必要的等待,因为循环在每个树级别都暂停等待,而不是立即处理添加到folderQueue的新文件夹 不知何故,我希望在执行WaitAll时,将添加到folderQueue的新文件夹包括在Task.WaitAll()等待的任务列表中。这可能吗

我正在尝试实现一个通用文件系统爬虫程序,例如,它能够枚举从给定根开始的所有子文件夹。我想使用async/await/Task范式来实现这一点

下面是到目前为止我的代码。它是有效的,但我怀疑它可以改进。特别是,带注释的
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}");
});