¿Cómo limitar la cantidad de operaciones de E/S asíncronas simultáneas?

Resuelto Grief Coder asked hace 12 años • 12 respuestas
// 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);
});

Aquí está el problema: inicia más de 1000 solicitudes web simultáneas. ¿Existe una manera sencilla de limitar la cantidad simultánea de estas solicitudes http asíncronas? Para que no se descarguen más de 20 páginas web en un momento dado. ¿Cómo hacerlo de la manera más eficiente?

Grief Coder avatar May 30 '12 04:05 Grief Coder
Aceptado

Definitivamente puedes hacer esto en las últimas versiones de async para .NET, usando .NET 4.5 Beta. La publicación anterior de 'usr' apunta a un buen artículo escrito por Stephen Toub, pero la noticia menos anunciada es que el semáforo asíncrono llegó a la versión Beta de .NET 4.5.

Si observa nuestra querida SemaphoreSlimclase (que debería usar ya que tiene más rendimiento que la original Semaphore), ahora cuenta con una WaitAsync(...)serie de sobrecargas, con todos los argumentos esperados: intervalos de tiempo de espera, tokens de cancelación, todos sus amigos de programación habituales: )

Stephen también escribió una publicación de blog más reciente sobre las nuevas ventajas de .NET 4.5 que aparecieron con la versión beta; consulte Novedades del paralelismo en .NET 4.5 Beta .

Por último, aquí hay un código de muestra sobre cómo usar SemaphoreSlim para la limitación de métodos asíncronos:

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

Por último, pero probablemente una mención digna, es una solución que utiliza programación basada en TPL. Puede crear tareas vinculadas a delegados en la TPL que aún no se han iniciado y permitir que un programador de tareas personalizado limite la simultaneidad. De hecho, hay un ejemplo de MSDN aquí:

Consulte también TaskScheduler .

Theo Yaung avatar May 30 '2012 06:05 Theo Yaung

Si tiene un IEnumerable (es decir, cadenas de URL) y desea realizar una operación vinculada de E/S con cada uno de estos (es decir, realizar una solicitud http asíncrona) simultáneamente Y, opcionalmente, también desea establecer el número máximo de URL concurrentes. Solicitudes de E/S en tiempo real: así es como puede hacerlo. De esta manera, no utiliza el grupo de subprocesos y otros, el método utiliza semáforos delgados para controlar el máximo de solicitudes de E/S simultáneas, similar a un patrón de ventana deslizante, una solicitud se completa, sale del semáforo y entra la siguiente.

uso:

await 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);
    }
Dogu Arslan avatar Jun 01 '2016 12:06 Dogu Arslan

Después del lanzamiento de .NET 6 (en noviembre de 2021), y para todas las aplicaciones excepto ASP.NET, la forma recomendada de limitar la cantidad de operaciones de E/S asincrónicas simultáneas es la Parallel.ForEachAsyncAPI, con la MaxDegreeOfParallelismconfiguración. Así es como se puede utilizar en la práctica:

// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", /*...*/ };
var client = new HttpClient();
var options = new ParallelOptions() { MaxDegreeOfParallelism = 20 };

// now let's send HTTP requests to each of these URLs in parallel
await Parallel.ForEachAsync(urls, options, async (url, cancellationToken) =>
{
    var html = await client.GetStringAsync(url, cancellationToken);
});

En el ejemplo anterior, la Parallel.ForEachAsynctarea se espera de forma asincrónica. También puede Waithacerlo de forma sincrónica si es necesario, lo que bloqueará el hilo actual hasta que se completen todas las operaciones asincrónicas. El síncrono Waittiene la ventaja de que en caso de errores, se propagarán todas las excepciones. Por el contrario, el awaitoperador propaga por diseño sólo la primera excepción. En caso de que esto sea un problema, puedes encontrar soluciones aquí .


Nota sobre ASP.NET (no oficial¹): La Parallel.ForEachAsyncAPI funciona lanzando muchos trabajadores (tareas) en el servidor ThreadPool, y todos los trabajadores invocan al bodydelegado en paralelo. Esto va en contra del consejo ofrecido en el artículo de MSDN Programación asíncrona: Introducción a Async/Await en ASP.NET :

Puedes iniciar un trabajo en segundo plano esperando Task.Run, pero no tiene sentido hacerlo. De hecho, eso perjudicará su escalabilidad al interferir con la heurística del grupo de subprocesos de ASP.NET. Si tiene trabajo relacionado con la CPU en ASP.NET, lo mejor que puede hacer es ejecutarlo directamente en el subproceso de solicitud. Como regla general, no ponga en cola el trabajo en el grupo de subprocesos en ASP.NET.

Por lo tanto, utilizarlo Parallel.ForEachAsyncen una aplicación ASP.NET podría dañar la escalabilidad de la aplicación . En aplicaciones ASP.NET, la concurrencia está bien, pero se debe evitar el paralelismo.

De las respuestas enviadas actualmente, solo la respuesta de Dogu Arslan es adecuada para aplicaciones ASP.NET, aunque no tiene un comportamiento ideal en caso de excepciones (en caso de error, es Taskposible que no se complete lo suficientemente rápido).

¹ La nota anterior sobre ASP.NET es mi sugerencia personal, basada en mi comprensión general de la tecnología. No es una guía oficial de Microsoft.

Theodor Zoulias avatar Oct 21 '2020 01:10 Theodor Zoulias