¿Cómo limitar la cantidad de operaciones de E/S asíncronas simultáneas?
// 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?
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 SemaphoreSlim
clase (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 .
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);
}
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.ForEachAsync
API, con la MaxDegreeOfParallelism
configuració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.ForEachAsync
tarea se espera de forma asincrónica. También puede Wait
hacerlo 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 Wait
tiene la ventaja de que en caso de errores, se propagarán todas las excepciones. Por el contrario, el await
operador 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.ForEachAsync
API funciona lanzando muchos trabajadores (tareas) en el servidor ThreadPool
, y todos los trabajadores invocan al body
delegado 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.ForEachAsync
en 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 Task
posible 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.