Я провел некоторое исследование, чтобы понять, как мы можем создать многопоточное приложение, которое запускается через дерево.
Чтобы выяснить, как это можно реализовать наилучшим образом, я создал тестовое приложение, которое запускается через мой диск 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:
По просьбе Мэтью Уотсона я сначала загрузил дерево в память, а затем прошел через него. Теперь загрузка дерева выполняется последовательно.
Результаты, однако, одинаковы. 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:
Прочитав обновление комментария Мэтью, я попытался добавить в программу следующий код:
ThreadPool.SetMinThreads(4000, 16);
ThreadPool.SetMaxThreads(4000, 16);
Это, однако, не меняет способ преобразования AsParallel. Тем не менее, первые 8 шагов выполняются за мгновение до замедления до 1 шага в секунду.
(Дополнительное примечание: в настоящее время я игнорирую исключения, возникающие, когда я не могу получить доступ к каталогу с помощью блока Try Catch вокруг Directory.GetDirectories())
Изменить 3:
Также меня больше всего интересует разница между Parallel.ForEach и AsParallel.ForAll, потому что для меня просто странно, что по какой-то причине второй создает один поток для каждой рекурсии, в то время как первый обрабатывает все примерно в 30 потоках. Максимум. (А также, почему MSDN предлагает использовать AsParallel, несмотря на то, что он создает столько потоков с таймаутом ~ 1 секунда)
Изменить 4:
Еще одна странная вещь, которую я узнал: Когда я пытаюсь установить значение MinThreads в пуле потоков выше 1023, оно, похоже, игнорирует значение и масштабируется примерно до 8 или 16: ThreadPool.SetMinThreads(1023, 16);
Тем не менее, когда я использую 1023, он делает первые 1023 элемента очень быстро, а затем возвращается к медленному темпу, который я испытывал все время.
Примечание. Также буквально создано более 1000 потоков (по сравнению с 30 для всего Parallel.ForEach).
Означает ли это, что Parallel.ForEach намного умнее в обработке задач?
Еще немного информации: этот код печатается дважды 8 - 8, когда вы устанавливаете значение выше 1023: (Когда вы устанавливаете значение 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() намного медленнее.