【发布时间】:2014-09-18 08:32:54
【问题描述】:
我一直在做一些调查,看看我们如何创建一个通过树运行的多线程应用程序。
为了找到如何以最佳方式实现这一点,我创建了一个测试应用程序,该应用程序通过我的 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”
来源:http://msdn.microsoft.com/en-us/library/dd997403(v=vs.110).aspx
有人知道为什么会这样吗?
编辑 1:
按照 Matthew Watson 的要求,我首先将树加载到内存中,然后再循环遍历它。现在树的加载是按顺序完成的。
然而结果是一样的。 Unparallelized 和 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 的执行方式。前 8 个步骤仍在瞬间执行,然后减速到 1 步/秒。
(额外说明,当我无法通过 Directory.GetDirectories() 周围的 Try Catch 块访问目录时,我忽略了发生的异常)
编辑 3:
另外,我主要感兴趣的是 Parallel.ForEach 和 AsParallel.ForAll 之间的区别,因为对我来说很奇怪,由于某种原因,第二个为它所做的每个递归创建一个线程,而第一个处理所有最多约 30 个线程。 (以及为什么 MSDN 建议使用 AsParallel,即使它创建了这么多线程,超时时间约为 1 秒)
编辑 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);
编辑 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() 这么慢。
【问题讨论】:
-
使用
ThreadPool.SetMinThreads(4000, 4000);,您将 IO 完成端口线程设置为一个疯狂的数字。改用ThreadPool.SetMinThreads(4000, 16);(SetMaxThreads()相同) -
我现在已经这样做了,但不知何故仍然会遇到相同的结果。对于模式 2,我看到每秒在资源监视器中弹出 1 个额外线程。此外,当我启用 Console.WriteLine 时,我看到它以每秒大约 1 级的速度在我的磁盘中移动。模式 1 和 3 仍然在不到 1 秒(内存中)内执行我的整个磁盘(80.173 个元素)
-
你能使用面向任务的版本吗——意味着你等待递归调用的任务,直到调用完成才开始新任务?这里的问题是,当您只有 4 个 CPU 内核之类的东西时,当这个问题是内存中且受 CPU 限制时,您很快就会将线程数淹没。
-
@Dean,我创建了第 4 个案例来测试您的解决方案,但也发现这与 Parallel.ForEach 一样快。我认为这与内存操作和 CPU 没有太大关系,因为我几乎看不到 CPU 活动。我认为这与 AsParallel().ForAll() 方法中某处的某种超时有关。
-
@Devedse,我认为这仍然会在启动时启动所有线程。我没有完全想到这一点,但我认为你会在 for 循环中放置某种等待,以便循环在递归完成之前不会继续。当它沿着目录树向下移动时,您会获得很多深度,并且可能会获得多个线程,但至少它不会一次性创建所有线程。您还可以使用 Thread.Current.ID 调查线程,也许记录每种模型创建了多少线程。
标签: c# multithreading performance foreach parallel-processing