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

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

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

    static void SearchAll()
    {
        var shares = new[] { @"\\share1\dir1", @"\\share2\dir2", @"\\share3\dir5" };
        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 для EnumerateFilesAsync не существует, я не уверен, что лучший способ обернуть эти синхронные вызовы, чтобы включить поиск по нескольким каталогам. Похоже, что эта проблема подходит для асинхронного / ожидающего из-за аспекта, связанного с сетью / вводом-выводом.

Ответы [ 2 ]

2 голосов
/ 17 апреля 2019

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

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

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

0 голосов
/ 17 апреля 2019

Если вы делаете запросы часто, а сетевые ресурсы обновляются нечасто, вы можете обменять память на скорость, сохраняя в памяти зеркало всех имен файлов в сетевых ресурсах, и запрашивать это зеркало вместо файловой системы.Вам потребуется несколько 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[] { @"\\share1\dir1", @"\\share2\dir2", @"\\share3\dir5" });

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

// On application end
RemoteWatcher1.Dispose();

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...