Как включить параллелизм в asp.net для поиска в удаленных каталогах

#c# #asp.net-mvc #asynchronous #network-share

#c# #asp.net-mvc #асинхронный #общий сетевой ресурс

Вопрос:

Мне нужно выполнить поиск в большом количестве общих сетевых ресурсов для заданного набора файлов в MVC.net применение. Это работает последовательно, но очень медленно.

Я могу использовать Parallel.ForEach в консольном приложении, и, похоже, оно работает хорошо, но параллельно.ForEach, похоже, не работает в Mvc.Net и, насколько я могу судить, рекомендуется использовать async / await.

     static void SearchAll()
    {
        var shares = new[] { @"\share1dir1", @"\share2dir2", @"\share3dir5" };
        var lookfor = new[] { "file.txt", "file2.txt", "file3.jpg", "file4.xml", "file5.zip" };
        var paths = new List<string>();
        var sw = System.Diagnostics.Stopwatch.StartNew();
        foreach(var share in shares)
        {
            var found = Search(share, lookfor);
            paths.AddRange(found);
        }
        Console.WriteLine($"Found {paths.Count} files in {sw.Elapsed}");
    }

    static List<string> Search(string share, IEnumerable<string> files)
    {
        List<string> found = new List<string>();
        foreach(var filename in files)
        {
            var path = Path.Combine(share, filename);
            if (File.Exists(path))
            {
                found.Add(path);
            }
        }
        return found;
    }
 

Я надеюсь, что смогу использовать async / await для поиска каталогов в MVC.NET Действие контроллера, но не смог заставить его работать. Поскольку нет File.ExistsAsync for EnumerateFilesAsync , я не уверен, что лучший способ обернуть эти синхронные вызовы, чтобы включить поиск по нескольким каталогам. Похоже, эта проблема подходит для async / await из-за привязки к сети / IO.

Комментарии:

1. Каковы ваши цели производительности? Достаточно ли ускорить поиск в 4 раза, или вам нужно улучшение x100 или что-то в этом роде?

2. Ищу улучшение в 2-6 раз. Я вижу улучшение в 4 раза в консольном приложении с использованием Parallel. Метод ForEach. Надеюсь на аналогичное с async / await.

3. Если ожидается, что эта операция не будет вызываться часто, я бы посоветовал использовать этот Parallel.ForEach подход, установив разумное значение для ParallelOptions.MaxDegreeOfParallelism . Если он вызывается часто, вам следует рассмотреть другие подходы.

Ответ №1:

Поскольку файла нет.ExistsAsync для EnumerateFilesAsync я не уверен, что лучший способ обернуть эти синхронные вызовы, чтобы включить поиск по нескольким каталогам. Похоже, эта проблема подходит для async / await из-за привязки к сети / IO.

К сожалению, да. Это операции на основе ввода-вывода и должны иметь асинхронные API, но Win32 API не поддерживает асинхронность для таких операций с каталогами. Любопытно, что уровень драйвера устройства выполняет (даже для локальных дисков), поэтому вся базовая поддержка есть; мы просто не можем до нее добраться.

Parallel.ForEach должно работать на ASP.NET ; это просто не рекомендуется. Это связано с тем, что это будет мешать ASP.NET эвристика пула потоков. Например, если вы выполняете большую Parallel операцию, другим входящим запросам, возможно, придется дольше ждать обработки из-за исчерпания пула потоков. Для этого есть некоторые способы смягчения, например, установка минимального количества потоков пула потоков по умолчанию плюс независимо от того, что у вас MaxDegreeOfParallelism есть (и обеспечение наличия только одного Parallel потока за раз). Или вы могли бы зайти так далеко, чтобы выделить перечисление файлов в отдельный (частный) вызов API, чтобы он существовал в своем собственном AppDomain на том же сервере со своим отдельным пулом потоков.

Комментарии:

1. Спасибо. Удаление вложенной параллели. ForEach и ограничение MaxDegreeOfParallelism, казалось, сделали свое дело. Кстати, я читал вашу книгу о параллелизме C #! Многому научился — спасибо!!

Ответ №2:

Если вы часто выполняете запросы, а сетевые ресурсы обновляются нечасто, вы можете обменять память на скорость, сохранив в памяти зеркало всех имен файлов в сетевых ресурсах и запрашивая это зеркало вместо файловой системы. Вам понадобится несколько FileSystemWatcher объектов, по одному для каждого сетевого ресурса. Каждый раз, когда приходит уведомление, будет создаваться задача для перечисления файлов измененного каталога. Таким образом, вы могли бы добиться увеличения производительности в 100 раз или более.

Вот реализация:

 public class RemoteWatcher : IDisposable
{
    private readonly DirectoryData[] _ddArray;
    private readonly Task[] _initializingTasks;

    public RemoteWatcher(string[] shares)
    {
        _ddArray = shares.Select(path =>
        {
            var dd = new DirectoryData();
            dd.Path = path;
            dd.Watcher = new FileSystemWatcher(path);
            dd.Watcher.EnableRaisingEvents = true;
            dd.Watcher.Created  = (s, e) => OnChangedAsync(path);
            dd.Watcher.Renamed  = (s, e) => OnChangedAsync(path);
            dd.Watcher.Changed  = (s, e) => OnChangedAsync(path);
            dd.Watcher.Deleted  = (s, e) => OnChangedAsync(path);
            dd.Watcher.Error  = (s, e) => OnChangedAsync(path);
            dd.InProgress = true;
            return dd;
        }).ToArray();
        // Start processing all directories in parallel
        _initializingTasks = shares.Select(ProcessDirectoryAsync).ToArray();
    }

    private DirectoryData GetDirectoryData(string path)
    {
        return _ddArray.First(dd => dd.Path == path);
    }

    private async void OnChangedAsync(string path)
    {
        var dd = GetDirectoryData(path);
        Task delayTask;
        lock (dd)
        {
            dd.Cts?.Cancel();
            dd.Cts = new CancellationTokenSource();
            delayTask = Task.Delay(200, dd.Cts.Token);
        }
        try
        {
            // Workaround for changes firing twice
            await delayTask.ConfigureAwait(false);
        }
        catch (OperationCanceledException) // A new change occured
        {
            return; // Let the new event continue
        }
        lock (dd)
        {
            if (dd.InProgress)
            {
                dd.HasChanged = true; // Let it finish and mark for restart
                return;
            }
        }
        // Start processing
        var fireAndForget = ProcessDirectoryAsync(path);
    }

    private Task ProcessDirectoryAsync(string path)
    {
        return Task.Run(() =>
        {
            var dd = GetDirectoryData(path);
            var fileNames = Directory.EnumerateFiles(path).Select(Path.GetFileName);
            var hash = new HashSet<string>(fileNames, StringComparer.OrdinalIgnoreCase);
            lock (dd)
            {
                dd.FileNames = hash; // It is backed by a volatile field
                dd.InProgress = false;
                if (dd.HasChanged)
                {
                    dd.HasChanged = false;
                    var fireAndForget = ProcessDirectoryAsync(path); // Restart
                }
            }
        });
    }

    public async Task<string[]> SearchAllAsync(params string[] fileNames)
    {
        await Task.WhenAll(_initializingTasks);
        return _ddArray.SelectMany(dd =>
            fileNames.Where(f => dd.FileNames.Contains(f))
            .Select(fileName => Path.Combine(dd.Path, fileName))
        ).ToArray();
    }

    public void Dispose()
    {
        foreach (var dd in _ddArray) dd.Watcher.Dispose();
    }

    private class DirectoryData
    {
        public string Path { get; set; }
        public FileSystemWatcher Watcher { get; set; }
        public bool HasChanged { get; set; }
        public bool InProgress { get; set; }
        private volatile HashSet<string> _fileNames;
        public HashSet<string> FileNames
        {
            get => _fileNames; set => _fileNames = value;
        }
        public CancellationTokenSource Cts { get; set; }
    }
}
 

Пример использования:

 public static RemoteWatcher RemoteWatcher1 {get; private set;}

// On application start
RemoteWatcher1 = new RemoteWatcher(new[] { @"\share1dir1", @"\share2dir2", @"\share3dir5" });

// Search
var results = RemoteWatcher1.SearchAllAsync(new[] { "file.txt", "file2.txt", "file3.jpg", "file4.xml", "file5.zip" }).Resu<

// On application end
RemoteWatcher1.Dispose();
 

Первые поиски будут отложены до тех пор, пока не будут обработаны все сетевые ресурсы. Последующие поиски не будут заблокированы изменениями файловой системы. Результаты могут быть немного устаревшими, поскольку используются предыдущие зеркальные данные, пока не завершится текущая обработка.