【问题标题】:Asynchronously Enumerate Folders异步枚举文件夹
【发布时间】:2016-01-03 17:48:00
【问题描述】:

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

以下是我目前的代码。它有效,但我怀疑它可以改进。特别是,带注释的Task.WaitAll 会在深层目录树中导致不必要的等待,因为循环在每个树级别暂停等待,而不是立即着手处理添加到folderQueue 的新文件夹。

不知何故,我想将添加到folderQueue 的新文件夹包含在Task.WaitAll() 正在等待的任务列表中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;
            }
        }
    }
}

【问题讨论】:

  • 我猜您正在尝试加快枚举文件夹的过程。您是否真的进行了测量以查看您获得了一些性能?请注意,没有真正的异步 API 可以枚举文件夹或文件(至少 AFAIK)。你所做的其实是使用更多线程来同步枚举文件夹,但是这些线程会花费大部分时间等待同步 IO 完成。
  • @YacoubMassad 是的,更快是这里的目标。实际上,我确实测量了我的解决方案与幼稚、顺序、同步枚举的性能,它肯定更快(尽管与直觉相反,在具有 12 个超核的机器上仅快约 3 倍;我期待更多改进)。我正在做的实际上是并行枚举文件夹 - 几个任务同时抓取不同文件夹的子文件夹。我相信 DirectoryInfo.GetDirectories 可以在不同的文件夹上并行执行(至少在 NTFS 中)。
  • 你试过高性能吗?

标签: c# asynchronous filesystems async-await directory


【解决方案1】:

理论上,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 线程,那么 single Task.Run 就足够了。

【讨论】:

  • +1,显然更好的解决方案。但请注意,如果只有一个目录不允许您访问,则以下所有内容将不再枚举。尝试访问不允许抛出异常的目录。
  • 我不太确定它显然是一个更好的解决方案,因为它不仅存在 René 发现的问题(在第一个“未经授权”错误后未能枚举任何内容),但它在我的测试中也比我原来的解决方案慢 3 倍。
  • 不过,关于竞态条件的有效点。我在我的真实代码中使用Interlocked。我的例子有点草率,因为它试图将问题提炼成最简单的 SO 表示。在我的实际代码中有更多的“绒毛”,包括更多的错误处理、锁定和其他保护。 :-)
  • 我也试过了。对我来说,它的解决方案真的很慢,而且它找到的文件夹比另一种方式少。也许我没有做错什么,但无论如何它变慢了。
【解决方案2】:

单独抓取和处理

尝试使用生产者-消费者模式。
这是一种在一个线程中爬取目录并在另一个线程中处理的方法。

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

Parallel.ForEach(collection.GetConsumingEnumerable(), dir =>
{
    // Do something with the current folder
    // e.g. Console.WriteLine($"{dir.FullName}");
});

【讨论】:

    【解决方案3】:

    这是我的建议。我使用通用的Concurrent*&lt;&gt; 类,所以我不必自己处理锁(尽管这不会自动提高性能)。

    然后我为每个文件夹启动一个任务并在ConcurrentBag&lt;Task&gt; 中排队。开始第一个任务后,我总是在包里等待第一个任务,如果没有其他任务等待,我就完成了。

    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;
                }
            }
        }
    }
    

    我还没有衡量这是否比您的解决方案更快。但我认为(正如 Yacoub Massad 所说),瓶颈将是 IO 系统本身,而不是您组织任务的方式

    【讨论】:

    • 是的,您的版本比我的版本效率高 20%(在具有 170+k 个文件夹的树上测试),因为您避免构建与任务列表分开的 DirectoryInfos 列表,并利用Concurrent 容器的自动锁定。我会接受你的回答。谢谢。
    • 顺便说一句,您的实现中甚至不需要folderQueue。没有与所需任务包分开的目录列表。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-07
    • 2018-04-21
    • 2022-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多