Tareas de continuación suspendidas al utilizar LimitedConcurrencyLevelTaskScheduler

Tengo mi trabajo en el uso de la TPL en C # (. NET 4.0).

He creado una API personalizada para facilitar la creación de solicitudes web y descargar el contenido (asincrónicamente, mediante tareas de continuación). Esa parte está funcionando bien.

El problema que tengo se produce cuando intento utilizar el LimitedConcurrencyLevelTaskScheduler (encontrado en las muestras para la programación paralela y en la documentación de MSDN para las tareas ) con la creación de tarea diferida. Si no está familiarizado con esa clase, todo lo que hace es limitar el grado de concurrencia de las tareas programadas a un número arbitrario.

Básicamente, quiero aplazar la creación de cadenas de tareas de solicitud web en una tarea programada por el LimitedConcurrencyLevelTaskScheduler para poder limitar el número de descargas simultáneas.

Como sugiere el sabio Stephen Toub , al diferir la creación de una Task , lo mejor es diseñar su API para devolver Func<Task> o Func<Task<TResult>> . He hecho esto.

Lamentablemente, mi programa se bloquea después de programar el primer conjunto de tareas simultáneas. Digamos que tengo mis tareas limitadas a 4 grados de concurrencia. En ese caso, se iniciarían 4 tareas y luego se bloquearía el programa. Las tareas nunca completarían.

He creado un ejemplo mínimo para ilustrar el problema simplemente. Estoy usando archivos lee en lugar de usar un WebRequest . He limitado los grados de concurrencia a 1.

 class Program { static Func<Task> GetReadTask() { return () => { Console.WriteLine("Opening file."); FileStream fileStream = File.Open("C:\\Users\\Joel\\Desktop\\1.txt", FileMode.Open); byte[] buffer = new byte[32]; Console.WriteLine("Beginning read."); return Task<int>.Factory.FromAsync(fileStream.BeginRead, fileStream.EndRead, buffer, 0, 32, null).ContinueWith(task => fileStream.Close()); }; } static void Main() { LimitedConcurrencyLevelTaskScheduler ts = new LimitedConcurrencyLevelTaskScheduler(1); TaskFactory factory = new TaskFactory(ts); int[] range = {1, 2, 3}; var tasks = range.Select(number => { Func<Task> getTask = GetReadTask(); return factory.StartNew(() => { var task = getTask(); task.Wait(); }); }); Task.WaitAll(tasks.ToArray()); } } 

Para aclarar lo que quiero decir con "se cuelga", esto es lo que parece el resultado.

 Opening file. Beginning read. 

Y entonces no se imprime nada más … para siempre.

¿Alguna pista sobre lo que está pasando?

¡Buena pregunta!

En primer lugar, no estoy seguro de LimitedConcurrencyLevelTaskScheduler es la solución académicamente correcta. Para que esto limite el número de solicitudes simultáneas a N, tiene que bloquear N tareas que tipo de derrotas el propósito de utilizar APM async llamadas en el primer lugar.

Dicho esto, es mucho más fácil de implementar que la alternativa. Usted necesitaría tener una cola de trabajo y mantener la cuenta del número de solicitudes de vuelo, y luego crear tareas de trabajo según sea necesario. Eso no es trivial para obtener la derecha y si el número N de peticiones simultáneas será pequeño, tener N hilos bloqueados no es el fin del mundo.

Por lo tanto, el problema con su código es que las tareas creadas dentro de otras tareas utilizan el planificador de la tarea principal. En realidad esto no es cierto para las tareas creadas con FromAsync ya que éstas usan la implementación de APM subyacente y por lo tanto son un poco diferentes.

Crea tareas en Main con:

 return factory.StartNew( () => { var task = getTask(); task.Wait(); } ); 

factory utiliza el LimitedConcurrencyLevelTaskScheduler( 1 ) , por lo que sólo una de estas tareas se puede ejecutar simultáneamente y que uno está esperando en la tarea devuelta por getTask() .

Por lo tanto, en GetReadTask llama Task<int>.Factory.FromAsync . Esto se ejecuta porque FromAsync no respeta el programador de la tarea primaria.

A continuación, crear una continuación con .ContinueWith(task => fileStream.Close()) . Esto crea una tarea que respeta el planificador de su padre. Dado que LimitedConcurrencyLevelTaskScheduler ya está ejecutando una tarea (la que está bloqueada en Main ), la continuación no se puede ejecutar y tiene un bloqueo.

La solución es ejecutar la continuación en un subproceso de grupo de subprocesos normal con TaskScheduler.Default . Entonces se ejecuta y el bloqueo se rompe.

Aquí está mi solución:

 static Task QueueReadTask( TaskScheduler ts, int number ) { Output.Write( "QueueReadTask( " + number + " )" ); return Task.Factory.StartNew( () => { Output.Write( "Opening file " + number + "." ); FileStream fileStream = File.Open( "D:\\1KB.txt", FileMode.Open, FileAccess.Read, FileShare.Read ); byte[] buffer = new byte[ 32 ]; var tRead = Task<int>.Factory.FromAsync( fileStream.BeginRead, fileStream.EndRead, buffer, 0, 32, null ); var tClose = tRead.ContinueWith( task => { Output.Write( "Closing file " + number + ". Read " + task.Result + " bytes." ); fileStream.Close(); } , TaskScheduler.Default ); tClose.Wait(); } , CancellationToken.None , TaskCreationOptions.None , ts ); } 

Y Main ahora se ve así:

 static void Main() { LimitedConcurrencyLevelTaskScheduler ts = new LimitedConcurrencyLevelTaskScheduler( 1 ); int[] range = { 1, 2, 3 }; var tasks = range.Select( number => { var task = QueueReadTask( ts, number ); return task.ContinueWith( t => Output.Write( "Number " + number + " completed" ) ); } ) .ToArray(); Output.Write( "Waiting for " + tasks.Length + " tasks: " + String.Join( " ", tasks.Select( t => t.Status ).ToArray() ) ); Task.WaitAll( tasks ); Output.Write( "WaitAll complete for " + tasks.Length + " tasks: " + String.Join( " ", tasks.Select( t => t.Status ).ToArray() ) ); } 

Hay un par de cosas a tener en cuenta:

Mover la task.Wait() en QueueReadTask hace más obvio que está bloqueando una tarea. Puede quitar la llamada FromAsync y la continuación y reemplazarlos por una llamada síncrona normal, ya que está bloqueando de todos modos.

La tarea devuelta de QueueReadTask puede tener continuaciones. De forma predeterminada, éstos se ejecutan bajo el programador predeterminado porque heredan el programador de la tarea principal y no el del antecedente. En este caso, no hay tarea padre, por lo que se utiliza el planificador predeterminado.

  • ¿Cómo administrar archivos en un dispositivo portátil MTP?
  • Comunicación entre 2 aplicaciones en el mismo dispositivo iOS / Android con Xamarin
  • ¿Es posible desarrollar una aplicación Android con C #?
  • Geolocalización, servicio y notificaciones locales
  • Agregue un niño al padre en el botón Haga clic en Xamarin.forms
  • Xamarin Studio Android App "aapt.exe" salió con el código -1073741819 error MSB6006:
  • UnityWebRequest Incorporación de datos de usuario y contraseña para la autenticación HTTP básica que no funciona en Android
  • "System.IO.FileNotFoundException: No se pudo cargar el ensamblado 'Xamarin.Android.Support.v13'" Después de reemplazar con la biblioteca de soporte v4
  • Android para MonoDevelop en Ubuntu
  • ¿Es posible escribir aplicaciones para Android en C #?
  • error en el cambio de tipo de letra en xamarin para android
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.