Как ограничить количество одновременных операций асинхронного ввода-вывода?

// let say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let send HTTP requests to each of these URLs in parallel
urls.AsParallel().ForAll(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
});

Вот проблема: он запускает 1000+ одновременных веб-запросов. Есть ли простой способ ограничить одновременную сумму этих асинхронных HTTP-запросов? Так что в любой момент времени загружается не более 20 веб-страниц. Как это сделать наиболее эффективным образом?

Ответ 1

Вы можете сделать это в последних версиях async для .NET, используя .NET 4.5 Beta. Предыдущее сообщение от "usr" указывает на хорошую статью, написанную Стивеном Тубом, но менее анонсированные новости состоят в том, что асинхронный семафор фактически превратил его в бета-версию .NET 4.5

Если вы посмотрите на наш любимый SemaphoreSlim класс (который вы должны использовать, поскольку он более совершенен, чем оригинальный Semaphore), теперь он может похвастаться серией перегрузок WaitAsync(...) со всеми ожидаемыми аргументами - интервалами таймаута, маркерами отмены, всеми вашими обычные друзья планирования:)

Стивен также написал более поздний пост в блоге о новых плюсах .NET 4.5, которые появились в бета-версии. Что нового для Parallelism в .NET 4.5 Beta.

Наконец, здесь приведен пример кода о том, как использовать SemaphoreSlim для дросселирования асинхронного метода:

public async Task MyOuterMethod()
{
    // let say there is a list of 1000+ URLs
    var urls = { "http://google.com", "http://yahoo.com", ... };

    // now let send HTTP requests to each of these URLs in parallel
    var allTasks = new List<Task>();
    var throttler = new SemaphoreSlim(initialCount: 20);
    foreach (var url in urls)
    {
        // do an async wait until we can schedule again
        await throttler.WaitAsync();

        // using Task.Run(...) to run the lambda in its own parallel
        // flow on the threadpool
        allTasks.Add(
            Task.Run(async () =>
            {
                try
                {
                    var client = new HttpClient();
                    var html = await client.GetStringAsync(url);
                }
                finally
                {
                    throttler.Release();
                }
            }));
    }

    // won't get here until all urls have been put into tasks
    await Task.WhenAll(allTasks);

    // won't get here until all tasks have completed in some way
    // (either success or exception)
}

Последнее, но, вероятно, достойное упоминание - это решение, использующее планирование на основе TPL. Вы можете создавать задачи, связанные с делегатом, в TPL, которые еще не были запущены, и позволить настраиваемому планировщику задач ограничить concurrency. На самом деле здесь есть образец MSDN:

См. также TaskScheduler.

Ответ 2

Если у вас есть IEnumerable (т.е. строки URL-адреса), и вы хотите выполнить операцию привязки ввода-вывода с каждым из них (т.е. сделать асинхронный HTTP-запрос) одновременно, и, возможно, вы также хотите установить максимальный количество одновременных запросов ввода-вывода в режиме реального времени, вот как вы можете это сделать. Таким образом, вы не используете пул потоков и т.д., Метод использует семафоры для управления максимальными параллельными запросами ввода-вывода, аналогичными шаблону скользящего окна, который завершается одним запросом, оставляя семафор и следующий.

использование: ожидание ForEachAsync (urlStrings, YourAsyncFunc, optionalMaxDegreeOfConcurrency);

public static Task ForEachAsync<TIn>(
        IEnumerable<TIn> inputEnumerable,
        Func<TIn, Task> asyncProcessor,
        int? maxDegreeOfParallelism = null)
    {
        int maxAsyncThreadCount = maxDegreeOfParallelism ?? DefaultMaxDegreeOfParallelism;
        SemaphoreSlim throttler = new SemaphoreSlim(maxAsyncThreadCount, maxAsyncThreadCount);

        IEnumerable<Task> tasks = inputEnumerable.Select(async input =>
        {
            await throttler.WaitAsync().ConfigureAwait(false);
            try
            {
                await asyncProcessor(input).ConfigureAwait(false);
            }
            finally
            {
                throttler.Release();
            }
        });

        return Task.WhenAll(tasks);
    }

Ответ 3

К сожалению,.NET Framework не хватает наиболее важных комбинаторов для организации параллельных задач async. Нет такой встроенной вещи.

Посмотрите на класс AsyncSemaphore, построенный самым уважаемым Стивеном Тубом. То, что вы хотите, называется семафором, и вам нужна асинхронная версия.

Ответ 4

Есть много подводных камней, и прямое использование семафора может быть сложным в случаях ошибок, поэтому я бы предложил использовать пакет Nuynet AsyncEnumerator вместо повторного изобретения колеса:

// let say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let send HTTP requests to each of these URLs in parallel
await urls.ParallelForEachAsync(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
}, maxDegreeOfParalellism: 20);

Ответ 5

Пример Тео Йаунг хорош, но есть вариант без списка ожидающих задач.

 class SomeChecker
 {
    private const int ThreadCount=20;
    private CountdownEvent _countdownEvent;
    private SemaphoreSlim _throttler;

    public Task Check(IList<string> urls)
    {
        _countdownEvent = new CountdownEvent(urls.Count);
        _throttler = new SemaphoreSlim(ThreadCount); 

        return Task.Run( // prevent UI thread lock
            async  () =>{
                foreach (var url in urls)
                {
                    // do an async wait until we can schedule again
                    await _throttler.WaitAsync();
                    ProccessUrl(url); // NOT await
                }
                //instead of await Task.WhenAll(allTasks);
                _countdownEvent.Wait();
            });
    }

    private async Task ProccessUrl(string url)
    {
        try
        {
            var page = await new WebClient()
                       .DownloadStringTaskAsync(new Uri(url)); 
            ProccessResult(page);
        }
        finally
        {
            _throttler.Release();
            _countdownEvent.Signal();
        }
    }

    private void ProccessResult(string page){/*....*/}
}

Ответ 6

Просто более краткая версия fooobar.com/questions/30257/...:

static async Task WhenAll(IEnumerable<Task> tasks, int maxThreadCount) {
    using (var guard = new SemaphoreSlim(initialCount: maxThreadCount)) {
        await Task.WhenAll(tasks.Select(async task => {
            await guard.WaitAsync();

            return task.ContinueWith(t => guard.Release());
        }));
    }
}

Ответ 7

SemaphoreSlim может быть очень полезным здесь. Здесь метод расширения, который я создал.

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxActionsToRunInParallel">Optional, max numbers of the actions to run in parallel,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxActionsToRunInParallel = null)
    {
        if (maxActionsToRunInParallel.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxActionsToRunInParallel.Value, maxActionsToRunInParallel.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all of the provided tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

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

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);

Ответ 8

Хотя 1000 задач могут быть поставлены в очередь очень быстро, библиотека Parallel Tasks может обрабатывать одновременные задачи, равные количеству процессорных ядер в машине. Это означает, что если у вас есть четырехъядерная машина, в данный момент будет выполняться только 4 задания (если вы не уменьшите MaxDegreeOfParallelism).

Ответ 9

Параллельные вычисления должны использоваться для ускорения операций, связанных с ЦП. Здесь мы говорим о связанных операциях ввода-вывода. Ваша реализация должна быть чисто асинхронной, если только вы не подавляете занятый одноядерный процессор на многоядерном процессоре.

EDIT Мне нравится предложение, сделанное usr для использования "асинхронного семафора" здесь.

Ответ 10

По сути, вы захотите создать действие или задачу для каждого URL-адреса, который вы хотите поразить, поместить в список и затем обработать этот список, ограничивая число, которое может обрабатываться параллельно.

Мой пост в блоге показывает, как это сделать как с задачами, так и с действиями, и предоставляет примерный проект, который вы можете скачать и запустить, чтобы увидеть оба В бою.

С помощью действий

При использовании действий вы можете использовать встроенную функцию .Net Parallel.Invoke. Здесь мы ограничиваем его параллельным запуском не более 20 потоков.

var listOfActions = new List<Action>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(() => CallUrl(localUrl)));
}

var options = new ParallelOptions {MaxDegreeOfParallelism = 20};
Parallel.Invoke(options, listOfActions.ToArray());

С задачами

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

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run, at most, the specified number of tasks in parallel.
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, CancellationToken cancellationToken = new CancellationToken())
    {
        await StartAndWaitAllThrottledAsync(tasksToRun, maxTasksToRunInParallel, -1, cancellationToken);
    }

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run the specified number of tasks in parallel.
    /// <para>NOTE: If a timeout is reached before the Task completes, another Task may be started, potentially running more than the specified maximum allowed.</para>
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="timeoutInMilliseconds">The maximum milliseconds we should allow the max tasks to run in parallel before allowing another task to start. Specify -1 to wait indefinitely.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, int timeoutInMilliseconds, CancellationToken cancellationToken = new CancellationToken())
    {
        // Convert to a list of tasks so that we don't enumerate over it multiple times needlessly.
        var tasks = tasksToRun.ToList();

        using (var throttler = new SemaphoreSlim(maxTasksToRunInParallel))
        {
            var postTaskTasks = new List<Task>();

            // Have each task notify the throttler when it completes so that it decrements the number of tasks currently running.
            tasks.ForEach(t => postTaskTasks.Add(t.ContinueWith(tsk => throttler.Release())));

            // Start running each task.
            foreach (var task in tasks)
            {
                // Increment the number of tasks currently running and wait if too many are running.
                await throttler.WaitAsync(timeoutInMilliseconds, cancellationToken);

                cancellationToken.ThrowIfCancellationRequested();
                task.Start();
            }

            // Wait for all of the provided tasks to complete.
            // We wait on the list of "post" tasks instead of the original tasks, otherwise there is a potential race condition where the throttler using block is exited before some Tasks have had their "post" action completed, which references the throttler, resulting in an exception due to accessing a disposed object.
            await Task.WhenAll(postTaskTasks.ToArray());
        }
    }

И затем, создав свой список задач и позвонив функции, чтобы они запускались, скажем, максимум 20 одновременных за раз, вы могли бы сделать это:

var listOfTasks = new List<Task>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(async () => await CallUrl(localUrl)));
}
await Tasks.StartAndWaitAllThrottledAsync(listOfTasks, 20);

Ответ 11

Старый вопрос, новый ответ. @vitidev имел блок кода, который был повторно использован почти нетронутым в проекте, который я рассмотрел. Обсудив несколько коллег, я спросил: "Почему бы вам просто не использовать встроенные методы TPL?" ActionBlock выглядит как победитель. https://msdn.microsoft.com/en-us/library/hh194773(v=vs.110).aspx. Вероятно, это не приведет к изменению какого-либо существующего кода, но, безусловно, обязательно примет этот nuget и повторит использование лучшей практики Mr. Softy для дросселирования parallelism.

Ответ 12

Вот решение, которое использует ленивый характер LINQ. Он функционально эквивалентен принятому ответу), но использует рабочие задачи вместо SemaphoreSlim. Сначала давайте заставим это работать без удушения. Первый шаг - преобразовать наши URL во множество задач.

string[] urls =
{
    "http://qaru.site/",
    "https://superuser.com",
    "https://serverfault.com",
    "https://meta.stackexchange.com",
    // ...
};
var httpClient = new HttpClient();
var tasks = urls.Select(async (url) =>
{
    return (Url: url, Html: await httpClient.GetStringAsync(url));
});

Второй шаг - одновременное выполнение await всех задач с использованием метода Task.WhenAll :

var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
    Console.WriteLine($"Url: {result.Url}, {result.Html.Length:#,0} chars");
}

Выход:

Url: /fooobar.com/..., 105.574 chars
Url: https://superuser.com, 126.953 chars
Url: https://serverfault.com, 125.963 chars
Url: https://meta.stackexchange.com, 185.276 chars
...

Внедрение Microsoft из Task.WhenAll мгновенно материализует предоставленный перечисляемый в массив массив, заставляя сразу запускать все задачи. Мы не хотим этого, потому что мы хотим ограничить количество одновременных асинхронных операций. Поэтому нам нужно реализовать альтернативу WhenAll, которая будет мягко и медленно перечислять наши перечислимые. Мы сделаем это, создав несколько рабочих задач (равных требуемой степени параллелизма), и каждая рабочая задача будет перечислять нашу перечисляемую одну задачу за раз, используя блокировку, чтобы гарантировать, что каждая URL-задача будет обработана. только одним рабочим заданием. Затем мы await завершаем все рабочие задачи и, наконец, возвращаем результаты после восстановления их порядка. Вот реализация:

public static async Task<T[]> WhenAll<T>(IEnumerable<Task<T>> tasks,
    int degreeOfParallelism)
{
    if (tasks is ICollection<Task<T>>) throw new ArgumentException(
        "The enumerable should not be materialized.", nameof(tasks));
    var results = new List<(int Index, T Result)>();
    var failed = false;
    using (var enumerator = tasks.GetEnumerator())
    {
        int index = 0;
        var workerTasks = Enumerable.Range(0, degreeOfParallelism)
        .Select(async _ =>
        {
            try
            {
                while (true)
                {
                    Task<T> task;
                    int localIndex;
                    lock (enumerator)
                    {
                        if (failed || !enumerator.MoveNext()) break;
                        task = enumerator.Current;
                        localIndex = index++;
                    }
                    var result = await task.ConfigureAwait(false);
                    lock (results) results.Add((localIndex, result));
                }
            }
            catch
            {
                lock (enumerator) failed = true;
                throw;
            }
        }).ToArray();
        await Task.WhenAll(workerTasks).ConfigureAwait(false);
    }
    return results.OrderBy(e => e.Index).Select(e => e.Result).ToArray();
}

... и вот что мы должны изменить в нашем исходном коде, чтобы добиться желаемого регулирования:

var results = await WhenAll(tasks, degreeOfParallelism: 2);

Существует разница в обработке исключений. Собственный Task.WhenAll ожидает завершения всех задач и объединяет все исключения. Приведенная выше реализация перестает ждать вскоре после завершения первой неисправной задачи.

Ответ 13

Используйте MaxDegreeOfParallelism, который можно указать в Parallel.ForEach():

var options = new ParallelOptions { MaxDegreeOfParallelism = 20 };

Parallel.ForEach(urls, options,
    url =>
        {
            var client = new HttpClient();
            var html = client.GetStringAsync(url);
            // do stuff with html
        });

Ответ 14

это не общее решение для асинхронного, но для HttpClient вы можете просто попробовать

System.Net.ServicePointManager.DefaultConnectionLimit = 20;