【问题标题】:Multithreaded Directory Looping in C#C#中的多线程目录循环
【发布时间】:2010-07-21 05:49:45
【问题描述】:

我正在尝试遍历所有文件和文件夹,并对所有具有特定扩展名的文件执行操作。这种方法效果很好,但我想让它成为多线程的,因为当完成数以万计的文件时,它真的很慢,我会使用多线程进行成像会加快速度。我只是不确定在这种情况下如何使用线程。

doStuff 从文件中读取属性(修改日期等)并将它们插入到 sqlite 数据库中。我在调用扫描方法之前启动事务,以便尽可能优化。

提供有关如何做到这一点的理论的答案与完整的工作代码答案一样好。

    private static string[] validTypes = { ".x", ".y", ".z", ".etc" };
    public static void scan(string rootDirectory)
    {
        try
        {

            foreach (string dir in Directory.GetDirectories(rootDirectory))
            {

                if (dir.ToLower().IndexOf("$recycle.bin") == -1)
                    scan(dir);
            }

            foreach (string file in Directory.GetFiles(rootDirectory))
            {

                if (!((IList<string>)validTypes).Contains(Path.GetExtension(file)))
                {
                    continue;
                }


                doStuff(file);
            }
        }
        catch (Exception)
        {
        }
    }

【问题讨论】:

  • 这是题外话,但你不应该捕获所有异常。
  • 是的,我同意。为了简单起见,我删除了那部分(因为这是一个 winforms 应用程序)
  • 为什么你会认为多线程会加快速度? 线程不会神奇地让你的磁盘运行得更快。线程可以让你的磁盘运行得变慢,因为磁盘控制器现在有更多的事情要做。您能解释一下为什么您认为多线程解决方案会更快吗?
  • @Eric:在实践中,线程可以加快 I/O。原因之一是线程没有 100% 地执行 I/O,因此额外的线程可以填补空白。另一个是 I/O 是延迟可能导致总带宽的未充分利用,而重叠的请求可能会完全填满管道。这就是理论:实践是它的基准测试速度更快。
  • 这是有道理的,因为当我插入 sqlite 数据库时,我可以从下一个文件开始。

标签: c# .net recursion


【解决方案1】:

假设doStuff 是线程安全的,并且您不需要等待整个扫描完成,您可以在线程池上同时调用doStuffscan,如下所示:

string path = file;
ThreadPool.QueueUserWorkItem(delegate { doStuff(path); });

您需要创建一个单独的局部变量,因为匿名方法会捕获file 变量本身,并且会在整个循环中看到它的变化。 (也就是说,如果线程池在循环继续下一个文件后才执行任务,就会处理错误的文件)

但是,阅读您的评论,这里的主要问题是磁盘 IO,所以我怀疑多线程不会有太大帮助。

请注意,Directory.GetFiles 对于具有大量文件的目录将执行缓慢。 (因为它需要分配一个数组来保存文件名)
如果您使用的是 .Net 4.0,则可以通过调用 EnumerateFiles method 来加快速度,它使用迭代器返回 IEnumerable&lt;string&gt;,在您运行循环时枚举目录。
您还可以通过传递SearchOption 参数来避免使用任一方法的递归scan 调用,如下所示:

foreach (string file in Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories))

这将递归扫描所有子目录,因此您只需要一个 foreach 循环。
请注意,这将加剧 GetFiles 的性能问题,因此您可能希望避免使用此 pre-.Net 4.0。

【讨论】:

  • 这是正确的方法,但您可能还希望多个线程进行搜索。
  • 所以即使只是读取文件,线程也无济于事?不过,我认为值得一试。
  • @SLaks:抱歉,按照我的理解,您建议使用单个线程进行搜索,为找到的每个文件排队 doStuff。如果是这样,我建议有多个搜索线程。这个想法是做你自己的递归。至于磁盘 I/O 和多线程,请参阅我对 Eric 的回复。
  • 嗯。目录中不应包含超过 20 或 30 个文件,因此这应该不是问题。你发布的最后一个 foreach 东西看起来很有趣,结合下面 Dan 的评论,我想我将不得不一起放弃多线程。
  • 正如我在第一句话中所说的,you can call both doStuff and scan on the ThreadPool
【解决方案2】:

在 IO 操作上使用多线程通常是一个错误的调用*。您可能有多个 CPU 或一个具有多个内核的 CPU;但一般情况下,您的硬盘无法同时读取或写入多个文件。这种事情通常需要序列化。

也就是说,在与 UI 线程分开的线程上执行此类工作是一种很好的做法。这样一来,当您的应用执行繁重的工作时,UI 仍能保持响应速度。

*我假设您的 scandoStuff 方法实际上是在硬盘上读取和/或写入数据。如果不是这种情况,那么并行化这段代码毕竟是有意义的。

【讨论】:

  • @Ramblingwood:您是在阅读文件的内容,还是只查看路径和/或DirectoryInfo/FileInfo 对象的属性?在大多数系统上,多线程也无法从硬盘读取。
  • 我明白了。我希望将来扩展到实际从硬盘读取,所以我想我不应该多线程。
【解决方案3】:

doStuffscan 到底是做什么的?除非它们是 CPU 密集型的,否则我会认为磁盘访问会成为瓶颈,并且如果有任何东西使它成为多线程可能会更慢。

【讨论】:

  • doStuff 从文件中读取属性(修改日期等)并将它们插入到 sqlite 数据库中。我在调用扫描方法之前开始事务,以便尽可能优化。
  • @Ramblingwood:您可以尝试只使用 2 个线程,一个将所有文件详细信息读入内存,另一个使用该信息写入数据库。然后,您可以衡量每个处理花费了多少时间,并确保您优化了正确的事情。
  • @Ramblingwood:看看 .NET 4.0 的阻塞队列 (msdn.microsoft.com/en-us/library/dd267312(VS.100).aspx)。生产者/消费者架构的好处在于,您可以轻松更改任一端的线程数以进行调整。
【解决方案4】:

附带说明,无需将 validTypes 转换为 IList&lt;string&gt;,因为数组在 .net 3.5+ 中实现了 IEnumerable&lt;T&gt;

其次,validTypes 可能更好地实现为HashSet,给你 O(1) 查找而不是 O(n) 与 Contains。也就是说,这可能不会影响这种情况下的性能,因为您的应用程序是 IO 绑定的,正如其他答案所指出的那样。

【讨论】:

  • O(1) 看起来不错,但如果您只需检查几个文件扩展名,散列字符串真的会更快吗?
【解决方案5】:

感谢所有回复的人。我最终选择的是

        foreach (string file in Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories))
        {
            if (!((IList<string>)validTypes).Contains(Path.GetExtension(file)))
            {
                continue;
            }
            string path = file;
            ThreadPool.QueueUserWorkItem(delegate { doStuff(path); });
        }

与之前需要几个小时相比,这大约需要 2 分钟。我认为大部分延迟都在数据库中,而不是文件 IO。

非常感谢大家!

【讨论】:

    猜你喜欢
    • 2021-06-01
    • 2013-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-07
    • 1970-01-01
    • 2011-06-17
    • 1970-01-01
    相关资源
    最近更新 更多