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

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

// now let's 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 веб-страницы загружаются в любой момент времени. Как сделать это наиболее эффективным способом?

12 ответов


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

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

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

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

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

    // now let's 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 можно создать задачи, связанные с делегатами, которые еще не запущены, и разрешить настраиваемому планировщику задач ограничить параллелизм. На самом деле, есть образец MSDN для него здесь:

см. также TaskScheduler .


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

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


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

использование: ожидание 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);
    }

пример Theo Yaung хорош, но есть вариант без списка задач ожидания.

 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){/*....*/}
}

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

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

// now let's 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);
}, maxDegreeOfParallelism: 20);

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);

                        // 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);

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


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

редактировать мне нравится предложение usr использовать "асинхронный семафор" здесь.


использовать 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
        });

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


просто более краткая версия https://stackoverflow.com/a/10810730/1186165:

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());
        }));
    }
}

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

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

Действиями

при использовании действий можно использовать встроенный .Net Параллельный.Вызвать функцию. Здесь мы ограничиваем его запуском не более 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's 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);