我一直在做一些调查,看看如何创建一个通过树运行的多线程应用程序。
为了找到如何以最佳方式实现这一点,我创建了一个测试应用程序,该应用程序在我的 C:\ 磁盘上运行并打开所有目录。
class Program
{
static void Main(string[] args)
{
//var startDirectory = @"C:\The folder\RecursiveFolder";
var startDirectory = @"C:\";
var w = Stopwatch.StartNew();
ThisIsARecursiveFunction(startDirectory);
Console.WriteLine("Elapsed seconds: " + w.Elapsed.TotalSeconds);
Console.ReadKey();
}
public static void ThisIsARecursiveFunction(String currentDirectory)
{
var lastBit = Path.GetFileName(currentDirectory);
var depth = currentDirectory.Count(t => t == '\\');
//Console.WriteLine(depth + ": " + currentDirectory);
try
{
var children = Directory.GetDirectories(currentDirectory);
//Edit this mode to switch what way of parallelization it should use
int mode = 3;
switch (mode)
{
case 1:
foreach (var child in children)
{
ThisIsARecursiveFunction(child);
}
break;
case 2:
children.AsParallel().ForAll(t =>
{
ThisIsARecursiveFunction(t);
});
break;
case 3:
Parallel.ForEach(children, t =>
{
ThisIsARecursiveFunction(t);
});
break;
default:
break;
}
}
catch (Exception eee)
{
//Exception might occur for directories that can't be accessed.
}
}
}
然而,我遇到的是,当在模式 3 (Parallel.ForEach) 下运行时,代码在大约 2.5 秒内完成(是的,我有一个 SSD ;))。在没有并行化的情况下运行代码大约需要 8 秒。在模式 2 (AsParalle.ForAll()) 下运行代码会花费近乎无限的时间。
在检查进程资源管理器时,我还遇到了一些奇怪的事实:
Mode1 (No Parallelization):
Cpu: ~25%
Threads: 3
Time to complete: ~8 seconds
Mode2 (AsParallel().ForAll()):
Cpu: ~0%
Threads: Increasing by one per second (I find this strange since it seems to be waiting on the other threads to complete or a second timeout.)
Time to complete: 1 second per node so about 3 days???
Mode3 (Parallel.ForEach()):
Cpu: 100%
Threads: At most 29-30
Time to complete: ~2.5 seconds
我发现特别奇怪的是 Parallel.ForEach 似乎忽略了仍在运行的任何父线程/任务,而 AsParallel().ForAll() 似乎等待上一个任务完成(这不会很快,因为所有父任务仍在等待子任务完成)。
我在 MSDN 上读到的内容是:“在可能的情况下,优先选择 ForAll 而不是 ForEach”
Source: http://msdn.microsoft.com/en-us/library/dd997403(v=vs.110).aspx
有谁知道为什么会这样?
Edit 1:
按照 Matthew Watson 的要求,我首先将树加载到内存中,然后再循环遍历它。现在树的加载是按顺序完成的。
但结果是一样的。 Unparallelized 和 Parallel.ForEach 现在在大约 0.05 秒内完成整个树,而 AsParallel().ForAll 仍然每秒只执行大约 1 步。
Code:
class Program
{
private static DirWithSubDirs RootDir;
static void Main(string[] args)
{
//var startDirectory = @"C:\The folder\RecursiveFolder";
var startDirectory = @"C:\";
Console.WriteLine("Loading file system into memory...");
RootDir = new DirWithSubDirs(startDirectory);
Console.WriteLine("Done");
var w = Stopwatch.StartNew();
ThisIsARecursiveFunctionInMemory(RootDir);
Console.WriteLine("Elapsed seconds: " + w.Elapsed.TotalSeconds);
Console.ReadKey();
}
public static void ThisIsARecursiveFunctionInMemory(DirWithSubDirs currentDirectory)
{
var depth = currentDirectory.Path.Count(t => t == '\\');
Console.WriteLine(depth + ": " + currentDirectory.Path);
var children = currentDirectory.SubDirs;
//Edit this mode to switch what way of parallelization it should use
int mode = 2;
switch (mode)
{
case 1:
foreach (var child in children)
{
ThisIsARecursiveFunctionInMemory(child);
}
break;
case 2:
children.AsParallel().ForAll(t =>
{
ThisIsARecursiveFunctionInMemory(t);
});
break;
case 3:
Parallel.ForEach(children, t =>
{
ThisIsARecursiveFunctionInMemory(t);
});
break;
default:
break;
}
}
}
class DirWithSubDirs
{
public List<DirWithSubDirs> SubDirs = new List<DirWithSubDirs>();
public String Path { get; private set; }
public DirWithSubDirs(String path)
{
this.Path = path;
try
{
SubDirs = Directory.GetDirectories(path).Select(t => new DirWithSubDirs(t)).ToList();
}
catch (Exception eee)
{
//Ignore directories that can't be accessed
}
}
}
Edit 2:
阅读马修评论的更新后,我尝试将以下代码添加到程序中:
ThreadPool.SetMinThreads(4000, 16);
ThreadPool.SetMaxThreads(4000, 16);
然而,这不会改变 AsParallel 的执行方式。前 8 个步骤仍在瞬间执行,然后减慢至 1 步/秒。
(额外注意,我目前忽略了当我无法通过 Directory.GetDirectories() 周围的 Try Catch 块访问目录时发生的异常)
Edit 3:
另外,我主要感兴趣的是 Parallel.ForEach 和 AsParallel.ForAll 之间的区别,因为对我来说,奇怪的是,出于某种原因,第二个为它执行的每个递归创建一个线程,而第一个一次处理大约 30 个线程中的所有内容最大限度。 (也是为什么 MSDN 建议使用 AsParallel,即使它创建了如此多的线程且超时时间约为 1 秒)
Edit 4:
我还发现了另外一件奇怪的事情:
当我尝试将线程池上的 MinThreads 设置为高于 1023 时,它似乎忽略了该值并缩小到 8 或 16 左右:
ThreadPool.SetMinThreads(1023, 16);
不过,当我使用 1023 时,它会非常快地执行前 1023 个元素,然后又回到我一直经历的慢速。
注意:实际上现在创建了超过 1000 个线程(相比之下,整个 Parallel.ForEach 线程创建了 30 个)。
这是否意味着 Parallel.ForEach 在处理任务方面更加智能?
更多信息,当您将值设置为 1023 以上时,此代码会打印两次 8 - 8:(当您将值设置为 1023 或更低时,它会打印正确的值)
int threadsMin;
int completionMin;
ThreadPool.GetMinThreads(out threadsMin, out completionMin);
Console.WriteLine("Cur min threads: " + threadsMin + " and the other thing: " + completionMin);
ThreadPool.SetMinThreads(1023, 16);
ThreadPool.SetMaxThreads(1023, 16);
ThreadPool.GetMinThreads(out threadsMin, out completionMin);
Console.WriteLine("Now min threads: " + threadsMin + " and the other thing: " + completionMin);
Edit 5:
根据 Dean 的要求,我创建了另一个案例来手动创建任务:
case 4:
var taskList = new List<Task>();
foreach (var todo in children)
{
var itemTodo = todo;
taskList.Add(Task.Run(() => ThisIsARecursiveFunctionInMemory(itemTodo)));
}
Task.WaitAll(taskList.ToArray());
break;
这也与 Parallel.ForEach() 循环一样快。所以我们仍然不知道为什么 AsParallel().ForAll() 慢得多。