为了找到最佳实现方法,我创建了一个测试应用程序,它遍历我的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秒(是的,我的固态硬盘很快;))。未使用并行化运行代码时,完成时间约为8秒。以模式2(AsParallel.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”。
来源:http://msdn.microsoft.com/en-us/library/dd997403(v=vs.110).aspx 有人知道这可能是为什么吗?
编辑1:
根据Matthew Watson的要求,我首先将树加载到内存中,然后再循环遍历。现在,树的加载是顺序进行的。
然而,结果是相同的。未并行化和Parallel.ForEach现在可以在约0.05秒内完成整个树,而AsParallel().ForAll仍然只能每秒完成1步。
代码:
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
}
}
}
编辑2:
在阅读了Matthew评论中的更新后,我尝试将以下代码添加到程序中:
ThreadPool.SetMinThreads(4000, 16);
ThreadPool.SetMaxThreads(4000, 16);
然而,这并不会改变AsParallel的执行方式。仍然会在放慢到每秒1步之前立即执行前8个步骤。
(额外说明,我目前正在忽略由于Try Catch块无法访问目录时发生的异常,周围有Directory.GetDirectories())
编辑3:
我主要感兴趣的是Parallel.ForEach和AsParallel.ForAll之间的区别,因为对我来说,第二个创建了一个线程,用于它所做的每次递归,而第一个则最多使用约30个线程处理所有内容。 (还有MSDN为什么建议使用AsParallel,即使它使用了约1秒的超时创建了这么多线程)
编辑4:
我发现另一件奇怪的事情: 当我尝试将Thread pool上的MinThreads设置为1023以上时,它似乎会忽略该值并缩小到大约8或16: ThreadPool.SetMinThreads(1023, 16);
当我使用1023时,它会非常快地完成前1023个元素,然后回到我一直经历的缓慢步伐。
注意:与整个Parallel.ForEach相比,现在实际上创建了1000多个线程。
这是否意味着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);
编辑5:
根据迪恩的要求,我创建了另一个案例来手动创建任务:
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()会慢那么多。
ThreadPool.SetMinThreads(4000, 4000);
,你将IO完成端口线程设置为一个疯狂的数字。尝试使用ThreadPool.SetMinThreads(4000, 16);
替代(对于SetMaxThreads()
同样适用)。 - Matthew Watson