Какое расширение Task‹T› я могу использовать для рекурсивного ожидания других задач?

Я не совсем уверен в эффективном способе сделать это. У меня есть файлы, в которых содержимое файла указывает на другие файлы, например:

A
|-- B
|   |-- C
|   |-- D
|       |-- E
|
|-- F
    |-- C

G
|-- H
|   |-- I
|
|-- D
|   |-- E
|
|-- J

Это продолжается для сотен тысяч и тысяч файлов; к счастью, глубина зависимостей очень мала, но, ради аргументов, потенциально она может быть N-уровневой, без циклических циклов. Моя цель - узнать полную зависимость каждого файла (сплющенного). Например:

  • A: (B, C, D, E, F) -- Обратите внимание, что «C» указан только один раз.
  • B: (C, D, E)
  • C: ()
  • D: (E)
  • E: ()
  • F: (C)
  • G: (D, E, H, I, J)
  • и т. д.

Сначала я начал с создания некоторой модели для отслеживания этой информации:

public class FileData
{
    public string FilePath { get; set; }

    public ISet<FileInfo> DependentUpon { get; set; }
}

Конечно, затем я создал List<FileData> для хранения обработанных файлов. Синхронное сканирование содержимого файлов для построения этого дерева зависимостей (а затем его выравнивание) заняло бы слишком много времени, поэтому я исследовал использование async/await, которое помогло ускорить процесс, но я хочу сделать его еще быстрее, прежде чем высвобождение его в производственной среде.

Моя попытка async/await намного быстрее, но все еще недостаточно эффективна.

public async Task<ICollection<FileData>> ProcessAsync(IEnumerable<FileInfo> files)
{
    var mappings = new Dictionary<FileInfo, FileData>();

    foreach (var file in files)
    {
        // Static Method that constructs an instance of the class
        // and utilizes async I/O to read the file line-by-line
        // to build any first level dependencies.
        var info = await FileData.CreateAsync(file);

        // Update progress + Other Properties

        mappings.Add(file, info);
    }

    // Go through the list and recursively add to the dependencies
    foreach (var item in list)
    {
        foreach (var dependency in GetAllDependencies(item, mappings))
        {
           file.DependentUpon.Add(dependency);
        }
    }
}

IEnumerable<FileInfo> GetAllDependencies(FileData data, IDictionary<FileInfo, FileData> mappings)
{
    foreach (var file in info.DependentUpon)
    {
        yield return file;

        foreach (var child in GetAllDependencies(mappings[file], mappings))
        {
            yield return child;
        }
    }
}

Это, конечно, несколько асинхронно, но все же очень синхронно и медленно, когда я пытаюсь получить иерархическую структуру (сплющенную). Я пытаюсь реорганизовать решение, чтобы оно работало намного быстрее, используя преимущества async/await при иерархическом поиске. Пока у меня есть только псевдоописание, и я понятия не имею, возможно ли это или как это правильно реализовать:

Создайте словарь FileInfo и Task<FileData> (так что я больше не жду создания экземпляров класса). После сканирования файла на наличие DependentUpon первого уровня я нахожу совпадающие задачи и продолжаю свою текущую задачу только после того, как эти задачи будут выполнены. Конечно, эти задачи имеют одни и те же инструкции, поэтому они будут помечены как завершенные только после завершения их зависимостей. Я хочу запустить все задачи одновременно. Например (просто пример, я не могу предсказать, какая задача будет завершена, когда):

  • Начать задачу А
  • Начать задачу Б
  • Сканировать файл A, DependentUpon (B, F)
  • Начать задачу C
  • Сканировать файл B, DependentUpon (C, D)
  • Задача A Дождитесь завершения задач B и F.
  • Начать задачу D
  • Сканировать файл C
  • ...
  • Задача D Дождитесь завершения задачи E.
  • Сканировать файл E, DependentUpon ()
  • Задача E завершена
  • Задача D завершена
  • Задача C завершена
  • Задача Б выполнена.
  • Начать задачу J
  • Задача F выполнена.
  • Задача А выполнена.
  • ...
  • Все задачи выполнены

person michael    schedule 12.01.2016    source источник
comment
Любая причина, почему вы await внутри цикла foreach? Не могли бы вы добавить CreateAsync задач в список, а затем Task.WhenAll после цикла?   -  person jamespconnor    schedule 13.01.2016
comment
Я удалил некоторые части для многословия, я обновлю некоторые комментарии, но я обновлю прогресс в первом цикле foreach.   -  person michael    schedule 13.01.2016
comment
Код, который вы предоставили, даже не компилируется должным образом. Существуют всевозможные переменные, появляющиеся без определения.   -  person Enigmativity    schedule 13.01.2016


Ответы (1)


Рассмотрите возможность использования Task. WhenAll‹> для одновременного ожидания задач, загружающих (рекурсивно) корневые элементы. Вы также можете отложить расширение списка зависимостей, что сократит время выполнения вашей функции и более эффективно использует память.

    public class FileData
    {
       public string FilePath { get; set; }

       public ISet<FileInfo> DependentUpon { get; set; }

       public IEnumerable<FileInfo> Dependencies {get; set;}
    }

Новое свойство Dependencies предоставляет список всех зависимых элементов. DependentUpon теперь содержит только непосредственные зависимости и не требует изменений.

    public async Task<ICollection<FileData>> ProcessAsync(IEnumerable<FileInfo> files)
    {
        var map = new Dictionary<FileInfo, Task<FileData>>();
        var tasks = files.Select(it => LoadFileDataAsync(it, map));
        return await Task.WhenAll(tasks);
    }

    static async Task<FileData> LoadFileDataAsync(FileInfo fileInfo, Dictionary<FileInfo, Task<FileData>> map)
    {
       // Load recursively FileData elements for all children 
       // storing the result in the map.

        Task<FileData> pendingTask;
        bool isNew;

        lock (map)
        {
            isNew = !map.TryGetValue(fileInfo, out pendingTask);
            if (isNew)
            {
                pendingTask = FileData.CreateAsync(fileInfo);
                map.Add(fileInfo, pendingTask);
            }
        }

        var data = await pendingTask;
        if (isNew)
        {
           // Assign an iterator traversing through the dependency graph
           // Note: parameters are captured by reference.
           data.Dependencies = ExpandDependencies(data.DependsUpon, map);
           if (data.DependsUpon.Count > 0)
           {
              // Recursively load child items
              var tasks = data.DependsUpon.Select(it => (Task)LoadFileDataAsync(it, map));
              await Task.WhenAll(tasks);
           }
        }
        return data;
    }


    static IEnumerable<FileInfo> ExpandDependencies(ISet<FileInfo> directDependencies, Dictionary<FileInfo, Task<FileData>> map)
    {

        if (directDependencies.Count == 0)
        {
            yield break;
        }

        // Depth-first graph traversal

        var visited = new HashSet<FileInfo>(map.Comparer); // check for duplicates
        var stack = new Stack<FileInfo>();

        foreach(var item in directDependencies)
        {
            stack.Push(item);
        }

        while(stack.Count > 0)
        {
            var info = stack.Pop();

            if (visited.Add(info))
            {
                yield return info;

                var data = map[info].Result;

                foreach (var child in data.DependsUpon)
                {
                    stack.Push(child);
                }
            }
        }
    }

Пример рабочего кода

person alexm    schedule 13.01.2016
comment
var data = await pendingTask; не дает вам ошибки использования неназначенной переменной. Я бы не поверил, что код скомпилирован, если бы вы не включили пример скрипта .NET кода для запуска. Странный. - person Scott Chamberlain; 13.01.2016
comment
@ScottChamberlain Он инициализируется с помощью map.TryGetValue(). - person alexm; 13.01.2016
comment
Ах!, я проглядел out. - person Scott Chamberlain; 13.01.2016