Programación Paralela en .NET Framework
Programación Paralela en .NET Framework
Programación Paralela en .NET Framework
NET Framework Muchos equipos y estaciones de trabajo tienen dos o cuatro ncleos (es decir, CPU) que permiten ejecutar varios subprocesos simultneamente. Se espera que los equipos en un futuro cercano tengan significativamente ms ncleos. Para aprovecharse del hardware de hoy y del maana, puede paralelizar el cdigo para distribuir el trabajo entre varios procesadores. En el pasado, la paralelizacin requera manipulacin de bajo nivel de los subprocesos y bloqueos. Visual Studio 2010 y .NET Framework 4 mejoran la compatibilidad para la programacin paralela proporcionando un nuevo runtime, nuevos tipos de biblioteca de clases y nuevas herramientas de diagnstico. Estas caractersticas simplifican el desarrollo en paralelo, de modo que pueda escribir cdigo paralelo eficaz, especfico y escalable de forma natural sin tener que trabajar directamente con subprocesos ni el bloque de subprocesos. La siguiente ilustracin proporciona una informacin general de alto nivel de la arquitectura de programacin paralela en .NET Framework 4.
Task Parallel Library .NET Framework 4 La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) es un conjunto de API y tipos pblicos de los espacios de nombres System.Threading.Tasks y System.Threading de .NET Framework versin 4. El propsito de la biblioteca TPL es aumentar la productividad de los desarrolladores al simplificar el proceso de agregar paralelismo y simultaneidad a las aplicaciones. La biblioteca TPL escala el grado de simultaneidad de forma dinmica para usar ms eficazmente todos los procesadores que estn disponibles. Adems, la TPL se encarga de la divisin del trabajo, la programacin de los subprocesos en ThreadPool, la compatibilidad con la cancelacin, la administracin de los estados y otros detalles de bajo nivel. Al utilizar la TPL, el usuario puede optimizar el rendimiento del cdigo mientras se centra en el trabajo para el que el programa est diseado. A partir de .NET Framework 4, la TPL es el modo preferido de escribir cdigo paralelo y multiproceso. Sin embargo, no todo el cdigo se presta para la paralelizacin; por ejemplo, si un bucle realiza solo una cantidad reducida de trabajo en cada iteracin o no se ejecuta para un gran nmero de iteraciones, la sobrecarga de la paralelizacin puede dar lugar a una ejecucin ms lenta del cdigo. Adems, al igual que cualquier cdigo multiproceso, la paralelizacin hace que la ejecucin del programa sea ms compleja. Aunque la TPL simplifica los escenarios de multithreading, recomendamos tener conocimientos bsicos sobre conceptos de subprocesamiento, por ejemplo, bloqueos, interbloqueos y condiciones de carrera, para usar la TPL eficazmente. Paralelismo de datos (Task Parallel Library) El paralelismo de datos hace referencia a los escenarios en los que la misma operacin se realiza simultneamente (es decir, en paralelo) en elementos de una coleccin o matriz de origen. Varias sobrecargas de los mtodos ForEach y For admiten el paralelismo de los datos con sintaxis imperativa en la clase System.Threading.Tasks.Parallel. En las operaciones paralelas de datos, se crean particiones de la coleccin de origen para que varios subprocesos puedan funcionar simultneamente en segmentos diferentes. TPL admite el paralelismo de datos a travs de la clase System.Threading.Tasks.Parallel. Esta clase proporciona las implementaciones paralelas basadas en mtodo de los bucles for y foreach (For y For Each en Visual Basic). Se escribe la lgica del bucle para un bucle Parallel.For o Parallel.ForEach de forma muy similar a como se escribira un bucle secuencial. No tiene que crear los subprocesos ni poner en la cola los elementos de trabajo. En bucles bsicos, no es preciso tomar bloqueos. TPL administra todo el trabajo de bajo nivel. En el siguiente ejemplo de cdigo se muestra un bucle foreach simple y su equivalente paralelo. // Sequential version
foreach (var item in sourceCollection) { Process(item); } // Parallel equivalent Parallel.ForEach(sourceCollection, item => Process(item)); Cuando un bucle paralelo se ejecuta, la TPL crea particiones del origen de datos para que el bucle pueda funcionar simultneamente en varias partes. En segundo plano, el programador de tareas crea particiones de la tarea segn los recursos del sistema y la carga de trabajo. Cuando es posible, el programador redistribuye el trabajo entre varios subprocesos y procesadores si se desequilibra la carga de trabajo. Los mtodos Parallel.ForEach y Parallel.For tienen varias sobrecargas que permiten detener o ejecutar la ejecucin de bucles, supervisar el estado del bucle en otros subprocesos, mantener el estado de subprocesos locales, finalizar los objetos de subprocesos locales, controlar el grado de simultaneidad, etc. Los tipos de aplicacin auxiliar que habilitan esta funcionalidad son ParallelLoopState, ParallelOptions y ParallelLoopResult, CancellationToken y CancellationTokenSource. Escribir un bucle Parallel.For simple
using System; using System.Diagnostics; using System.Threading.Tasks; namespace MultiplyMatrices { internal class Program { #region Sequential_Loop private static void MultiplyMatricesSequential(double[,] matA, double[,] matB, double[,] result) { int matACols = matA.GetLength(1); int matBCols = matB.GetLength(1); int matARows = matA.GetLength(0); for (int i = 0; i < matARows; i++) { for (int j = 0; j < matBCols; j++) { for (int k = 0; k < matACols; k++) { result[i, j] += matA[i, k] * matB[k, j]; } } } } #endregion #region Parallel_Loop private static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result) { int matACols = matA.GetLength(1); int matBCols = matB.GetLength(1); int matARows = matA.GetLength(0); // A basic matrix multiplication. // Parallelize the outer loop to partition the source array by rows. Parallel.For(0, matARows, i => { for (int j = 0; j < matBCols; j++) { // Use a temporary to improve parallel performance. double temp = 0; for (int k = 0; k < matACols; k++) { temp += matA[i, k] * matB[k, j]; } result[i, j] = temp; } }); // Parallel.For } #endregion #region Main private static void MainMatrix(string[] args) { // Set up matrices. Use small values to better view // result matrix. Increase the counts to see greater // speedup in the parallel loop vs. the sequential loop. int colCount = 180; int rowCount = 2000; int colCount2 = 270; double[,] m1 = InitializeMatrix(rowCount, colCount);
double[,] m2 = InitializeMatrix(colCount, colCount2); double[,] result = new double[rowCount, colCount2]; // First do the sequential version. Console.WriteLine("Executing sequential loop..."); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); MultiplyMatricesSequential(m1, m2, result); stopwatch.Stop(); Console.WriteLine("Sequential loop time in milliseconds: {0}", stopwatch.ElapsedMilliseconds); // For the skeptics. OfferToPrint(rowCount, colCount2, result); // Reset timer and results matrix. stopwatch.Reset(); result = new double[rowCount, colCount2]; // Do the parallel loop. Console.WriteLine("Executing parallel loop..."); stopwatch.Start(); MultiplyMatricesParallel(m1, m2, result); stopwatch.Stop(); Console.WriteLine("Parallel loop time in milliseconds: {0}", stopwatch.ElapsedMilliseconds); OfferToPrint(rowCount, colCount2, result); // Keep the console window open in debug mode. Console.WriteLine("Press any key to exit."); Console.ReadKey(); } #endregion #region Helper_Methods private static double[,] InitializeMatrix(int rows, int cols) { double[,] matrix = new double[rows, cols]; Random r = new Random(); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { matrix[i, j] = r.Next(100); } } return matrix; } private static void OfferToPrint(int rowCount, int colCount, double[,] matrix) { Console.WriteLine("Computation complete. Print results? y/n"); char c = Console.ReadKey().KeyChar; if (c == 'y' || c == 'Y') { Console.WindowWidth = 180; Console.WriteLine(); for (int x = 0; x < rowCount; x++) { Console.WriteLine("ROW {0}: ", x); for (int y = 0; y < colCount; y++) { Console.Write("{0:#.##} ", matrix[x, y]); } Console.WriteLine(); } } } #endregion } }
Puede utilizar la sobrecarga ms bsica del mtodo For si no necesita cancelar ni interrumpir las iteraciones, ni mantener un estado local de subproceso. Al paralelizar un cdigo, incluidos los bucles, un objetivo importante consiste en hacer tanto uso de los procesadores como sea posible, sin excederse hasta el punto de que la sobrecarga del procesamiento en paralelo anule las ventajas en el rendimiento. En este ejemplo determinado, solamente se paraleliza el bucle exterior, ya que en el bucle interior no se realiza demasiado trabajo. La combinacin de una cantidad pequea de trabajo y los efectos no deseados en la memoria cach puede producir la degradacin del rendimiento en los bucles paralelos anidados. Por consiguiente, paralelizar el bucle exterior solo es la mejor manera de maximizar las ventajas de simultaneidad en la mayora de los sistemas. Delegado El tercer parmetro de esta sobrecarga de For es un delegado de tipo Action<int> en C# o Action(Of Integer) en Visual Basic. Un delegado Action siempre devuelve void, tanto si no tiene parmetros como si tiene uno o diecisis. En Visual Basic, el comportamiento de Action se define con Sub. En el ejemplo se utiliza
una expresin lambda para crear el delegado, pero tambin se puede crear de otras formas. Para obtener ms informacin, vea Expresiones lambda en PLINQ y TPL. Valor de iteracin El delegado toma un nico parmetro de entrada cuyo valor es la iteracin actual. El runtime proporciona este valor de iteracin y su valor inicial es el ndice del primer elemento del segmento (particin) del origen que se procesa en el subproceso actual. Si requiere ms control sobre el nivel de simultaneidad, utilice una de las sobrecargas que toma un parmetro de entrada System.Threading.Tasks.ParallelOptions, como: Parallel.For(Int32, Int32, ParallelOptions, Action<Int32, ParallelLoopState>). Valor devuelto y control de excepciones For devuelve un objeto System.Threading.Tasks.ParallelLoopResult cuando se han completado todos los subprocesos. Este valor devuelto es til si se detiene o se interrumpe la iteracin del bucle de forma manual, ya que ParallelLoopResult almacena informacin como la ltima iteracin que se ejecut hasta finalizar. Si se producen una o ms excepciones en uno de los subprocesos, se inicia System.AggregateException. En el cdigo de este ejemplo, no se usa el valor devuelto de For. Anlisis y rendimiento Puede utilizar el Asistente de rendimiento para ver el uso de la CPU en el equipo. Como experimento, aumente el nmero de columnas y filas en las matrices. Cuanto mayores son las matrices, mayor es la diferencia de rendimiento entre las versiones en paralelo y en serie del clculo. Si la matriz es pequea, la versin en serie se ejecutar ms rpidamente debido a la sobrecarga de la configuracin del bucle paralelo. Las llamadas sincrnicas a los recursos compartidos, como la consola o el sistema de archivos, degradarn de forma significativa el rendimiento de un bucle paralelo. Al medir el rendimiento, intente evitar llamadas como Console.WriteLine dentro del bucle. Detener o interrumpir un bucle Parallel.For
using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; namespace StopOrBreak { internal class Test { private static void Main_StopOrBreak(string[] args) { StopLoop(); BreakAtThreshold(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); } private static void StopLoop() { Console.WriteLine("Stop loop..."); double[] source = MakeDemoSource(1000, 1); var results = new ConcurrentStack<double>(); Parallel.For(0, source.Length, (i, loopState) => { if (i < 100) { double d = Compute(source[i]); results.Push(d); } else { loopState.Stop(); return; } } // Close lambda expression. ); // Close Parallel.For Console.WriteLine("Results contains {0} elements", results.Count); } private static void BreakAtThreshold() { double[] source = MakeDemoSource(10000, 1.0002);
var results = new ConcurrentStack<double>(); Parallel.For(0, source.Length, (i, loopState) => { double d = Compute(source[i]); results.Push(d); if (d > .2) { // Might be called more than once! loopState.Break(); Console.WriteLine("Break called at iteration {0}. d = {1} ", i, d); Thread.Sleep(1000); } }); Console.WriteLine("results contains {0} elements", results.Count); } private static double Compute(double d) { return Math.Sqrt(d); } private static double[] MakeDemoSource(int size, double valToFind) { var result = new double[size]; double initialval = .01; for (int i = 0; i < size; i++) { initialval *= valToFind; result[i] = initialval; } return result; } } }
En un bucle ParallelFor() u [Overload:System.Threading.Tasks.Parallel.Parallel.ForEach`1], no se puede usar la misma instruccin break o Exit que se utiliza en un bucle secuencial porque estas construcciones de lenguaje son vlidas para los bucles, y un "bucle" paralelo es realmente un mtodo, no un bucle. En su lugar, se usan los mtodos Break o Stop. Algunas de las sobrecargas de Parallel.For aceptan Action<int, ParallelLoopState> (Action(Of Integer, ParallelLoopState) en Visual Basic) como parmetro de entrada. El runtime crea en segundo plano el objeto ParallelLoopState, al que puede dar cualquier nombre que desee en la expresin lambda. En el siguiente ejemplo, el mtodo requiere slo 100 valores de la secuencia de origen y no importa qu elementos se recuperan. En este caso se usa el mtodo Stop, porque indica todas las iteraciones del bucle (incluidas las que comenzaron antes de la iteracin actual en otros subprocesos), para detenerse en cuanto sea conveniente. En el segundo mtodo se recuperan todos los elementos hasta un ndice especificado en la secuencia de origen. En este caso, se llama a Break, porque cuando se llega al ndice en un subproceso, es posible que todava no se hayan procesado los elementos anteriores en el origen. La interrupcin har que otros subprocesos abandonen el trabajo en segmentos posteriores (si estn ocupados en alguno) y que completen el procesamiento de todos los elementos anteriores antes de salir del bucle. Es importante entender que despus de llamar a Stop o Break, otros subprocesos en un bucle pueden seguir ejecutndose durante algn tiempo, pero esto no est bajo el control del desarrollador de la aplicacin. Puede usar la propiedad ParallelLoopState.IsStopped para comprobar si el bucle se ha detenido en otro subproceso. En el siguiente ejemplo, si IsStopped es true, no se escriben ms datos en la coleccin. Escribir un bucle Parallel.For que tenga variables locales de subproceso Si se usan datos locales de subproceso, se puede evitar la sobrecarga de sincronizar un nmero grande de accesos al estado compartido. En lugar de escribir en un recurso compartido en cada iteracin, calcula y almacena el valor hasta que se completan todas las iteraciones de la tarea. A continuacin, puede escribir el resultado final una vez en el recurso compartido o pasarlo a otro mtodo.
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace ThreadLocalFor { class Test { static void Main() { int[] nums = Enumerable.Range(0, 125000000).ToArray(); long total = 0; Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) => { subtotal += nums[j]; return subtotal;
}, (x) => Interlocked.Add(ref total, x) ); Console.WriteLine("The total is {0}", total); Console.WriteLine("Press any key to exit"); Console.ReadKey(); } } }
Los dos primeros parmetros de cada mtodo For especifican los valores de iteracin inicial y final. En esta sobrecarga del mtodo, el tercer parmetro es donde inicializa el estado local. " Estado local" en este contexto significa una variable cuya duracin se extiende desde inmediatamente antes de la primera iteracin del bucle en el subproceso actual hasta inmediatamente despus de la ltima iteracin. El tipo del tercer parmetro es Func<TResult>, donde TResult es el tipo de la variable que almacenar el estado local del subproceso. Tenga en cuenta que, en este ejemplo, se usa una versin genrica del mtodo y el parmetro de tipo es long (Long en Visual Basic). El parmetro de tipo indica al compilador el tipo de la variable temporal que se usar para almacenar el estado local del subproceso. La expresin () => 0 (Function() 0 en Visual Basic) de este ejemplo significa que la variable local de subproceso se inicializa en cero. Si el parmetro de tipo es un tipo de referencia o un tipo de valor definido por el usuario, este ejemplo de Func se parecera al siguiente: () => new MyClass()
El cuarto parmetro de tipo es donde define la lgica del bucle. IntelliSense muestra que tiene un tipo de Func<int, ParallelLoopState, long, long> o Func(Of Integer, ParallelLoopState, Long, Long). La expresin lambda espera tres parmetros de entrada en este mismo orden que corresponde a estos tipos. El ltimo parmetro de tipo es el tipo devuelto. En este caso, el tipo es long porque es lo que se especific en el parmetro de tipo For. Llamamos a esa variable subtotal en la expresin lambda y la devolvemos. El valor devuelto se utiliza para inicializar el subtotal en cada iteracin subsiguiente. Tambin puede considerar este ltimo parmetro simplemente como un valor que se pasa a cada iteracin y despus al delegado localFinally cuando se completa la ltima iteracin. El quinto parmetro es donde se define el mtodo al que se llamar una vez, cuando todas las iteraciones de este subproceso se hayan completado. El tipo del parmetro de entrada corresponde de nuevo al parmetro de tipo del mtodo For y al tipo que devuelve la expresin lambda del cuerpo. En este ejemplo, el valor se agrega a una variable en el mbito de clase de una manera segura para subprocesos. Al usar una variable local de subproceso, hemos evitado escribir en esta variable de clase en cada iteracin de cada subproceso. Escribir un bucle Parallel.ForEach que tenga variables locales de subproceso Para utilizar una variable local de subproceso en un bucle ForEach, debe utilizar la versin del mtodo que toma dos parmetros type. El primer parmetro especifica el tipo del elemento de origen y el segundo parmetro especifica el tipo de la variable local de subproceso. El primer parmetro de entrada es el origen de datos y el segundo es la funcin que inicializar la variable local de subproceso. El tercer parmetro de entrada es un Func<T1, T2, T3, TResult> que invoca el bucle paralelo en cada iteracin. Se proporciona el cdigo para el delegado y el bucle pasa los parmetros de entrada. Los parmetros de entrada son el elemento vigente, una variable ParallelLoopState que permite examinar el estado del bucle, y la variable local de subproceso. Devuelve la variable local de subproceso y, a continuacin, el mtodo pasa a la iteracin siguiente de esta particin. Esta variable es distinta en todas las particiones del bucle. El ltimo parmetro de entrada del mtodo ForEach es el delegado Action<T> que el mtodo invocar cuando todos los bucles se hayan completado. El mtodo proporciona el valor final de la variable local de subproceso para este subproceso (o particin del bucle) y proporciona el cdigo que captura el valor final y realiza cualquier accin necesaria para combinar el resultado de esta particin con los resultados de las otras particiones. Como el tipo de delegado es Action<T>, no hay valor devuelto.
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace ThreadLocalForEach { internal class Test { private static void Main() { int[] nums = Enumerable.Range(0, 1000000).ToArray(); long total = 0; // First type parameter is the type of the source elements // Second type parameter is the type of the local data (subtotal) Parallel.ForEach<int, long>(nums, // source collection () => 0, // method to initialize the local variable (j, loop, subtotal) => // method invoked by the loop on each iteration {
subtotal += nums[j]; //modify local variable return subtotal; // value to be passed to next iteration }, // Method to be executed when all loops have completed. // finalResult is the final value of subtotal. supplied by the ForEach method. (finalResult) => Interlocked.Add(ref total, finalResult) ); Console.WriteLine("The total from Parallel.ForEach is {0}", total); Console.WriteLine("Press any key to exit"); Console.ReadKey(); } } }
Cancelar un bucle Parallel.For o ForEach Los mtodos Parallel.ForEach y Parallel.For admiten la cancelacin a travs del uso de tokens de cancelacin. Para obtener ms informacin sobre la cancelacin en general, vea Cancelacin. En un bucle paralelo, se proporciona CancellationToken al mtodo en el parmetro ParallelOptions y despus se agrega la llamada paralela en un bloque try-catch. Si el token que seala la cancelacin es el mismo que se especifica en la instancia de ParallelOptions, el bucle paralelo producir una OperationCanceledException nica en la cancelacin. Si algn otro token produce la cancelacin, el bucle producir una AggregateException con OperationCanceledException como InnerException.
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelParallelLoops { internal class Program { private static void Main() { int[] nums = Enumerable.Range(0, 10000000).ToArray(); CancellationTokenSource cts = new CancellationTokenSource(); // Use ParallelOptions instance to store the CancellationToken ParallelOptions po = new ParallelOptions(); po.CancellationToken = cts.Token; po.MaxDegreeOfParallelism = System.Environment.ProcessorCount; Console.WriteLine("Press any key to start. Press 'c' to cancel."); Console.ReadKey(); // Run a task so that we can cancel from another thread. Task.Factory.StartNew(() => { if (Console.ReadKey().KeyChar == 'c') cts.Cancel(); Console.WriteLine("press any key to exit"); }); try { Parallel.ForEach(nums, po, (num) => { double d = Math.Sqrt(num); Console.WriteLine("{0} on {1}", d, Thread.CurrentThread.ManagedThreadId); po.CancellationToken.ThrowIfCancellationRequested(); }); } catch (OperationCanceledException e) { Console.WriteLine(e.Message); } Console.ReadKey(); } } }
Controlar excepciones en bucles paralelos Las sobrecargas ParallelFor()y ForEach() no tienen ningn mecanismo especial para controlar las excepciones que puedan iniciarse. A este respecto, se asemejan a bucles for y foreach normales (For y For Each en Visual Basic). Cuando agregue su propia lgica de control de excepciones a los bucles paralelos, tenga en cuenta la posibilidad de que se inicien excepciones similares en varios subprocesos al mismo tiempo, as como el caso de que una excepcin iniciada en un subproceso puede hacer que se inicie otra excepcin en otro subproceso.
Puede administrar ambos casos si encapsula todas las excepciones del bucle en System.AggregateException. En el ejemplo siguiente se muestra un posible enfoque.
using System; using System.Collections.Concurrent; using System.Threading.Tasks; namespace Paralell_Practices { internal class ExceptionDemo2 { private static void Main(string[] args) { // Create some random data to process in parallel. // There is a good probability this data will cause some exceptions to be thrown. byte[] data = new byte[5000]; Random r = new Random(); r.NextBytes(data); try { ProcessDataInParallel(data); } catch (AggregateException ae) { // This is where you can choose which exceptions to handle. foreach (var ex in ae.InnerExceptions) { if (ex is ArgumentException) Console.WriteLine(ex.Message); else throw ex; } } Console.WriteLine("Press any key to exit."); Console.ReadKey(); } private static void ProcessDataInParallel(byte[] data) { // Use ConcurrentQueue to enable safe enqueueing from multiple threads. var exceptions = new ConcurrentQueue<Exception>(); // Execute the complete loop and capture all exceptions. Parallel.ForEach(data, d => { try { // Cause a few exceptions, but not too many. if (d < 0x3) throw new ArgumentException( String.Format("value is {0:x}. Elements must be greater than 0x3.", d)); else Console.Write(d + " "); } // Store the exception and continue with the loop. catch (Exception e) { exceptions.Enqueue(e); } }); // Throw the exceptions here after the loop completes. if (exceptions.Count > 0) throw new AggregateException(exceptions); } } }
Acelerar cuerpos de bucle pequeos Cuando un bucle For() tiene un cuerpo pequeo, puede registrar un rendimiento ms lento que el del bucle secuencial equivalente. Este rendimiento ms lento es consecuencia de la sobrecarga en la participacin de los datos y el costo de invocar un delegado en cada iteracin del bucle. Para hacer frente a estos escenarios, la clase Partitioner proporciona el mtodo Create, que permite proporcionar un bucle secuencial para el cuerpo de delegado de modo que el delegado solo se invoque una vez por particin, en lugar de una vez por iteracin.
using System; using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; namespace Paralell_Practices
{ internal class Accelerate { private static void Main() { // Source must be array or IList. var source = Enumerable.Range(0, 100000).ToArray(); // Partition the entire source array. var rangePartitioner = Partitioner.Create(0, source.Length); double[] results = new double[source.Length]; // Loop over the partitions in parallel. Parallel.ForEach(rangePartitioner, (range, loopState) => { // Loop over each range element without a delegate invocation. for (int i = range.Item1; i < range.Item2; i++) { results[i] = source[i]*Math.PI; } }); Console.WriteLine("Operation complete. Print results? y/n"); char input = Console.ReadKey().KeyChar; if (input == 'y' || input == 'Y') { foreach (double d in results) { Console.Write("{0} ", d); } } } } }
El enfoque mostrado en este ejemplo es til cuando el bucle realiza una cantidad de trabajo mnima. Cuando el trabajo se vuelve ms costoso en los clculos, obtendr probablemente un rendimiento igual o mejor si usa un bucle For o ForEach con el particionador predeterminado. Recorrer en iteracin directorios con la clase paralela En muchos casos, la iteracin de archivo es una operacin que se puede paralelizar fcilmente. El tema Cmo: Recorrer en iteracin directorios con PLINQ muestra la manera ms fcil de realizar esta tarea en muchos escenarios. Sin embargo, pueden surgir complicaciones cuando el cdigo tiene que tratar con los muchos tipos de excepciones que pueden surgir al obtener acceso al sistema de archivos. En el ejemplo siguiente se muestra un enfoque para el problema. Usa una iteracin basada en la pila para recorrer todos los archivos y carpetas en un directorio especificado y habilita el cdigo para detectar y controlar diversas excepciones. Por supuesto, la forma de controlar las excepciones depende de usted. En el ejemplo siguiente la iteracin en los directorios se realiza de forma secuencial, pero el procesamiento de los archivos se realiza en paralelo. Este enfoque es probablemente el mejor cuando hay una tasa alta de directorios y archivos. Tambin es posible ejecutar la iteracin de directorio y obtener acceso a cada archivo secuencialmente. Probablemente no es eficaz paralelizar ambos bucles a menos que est dirigido especficamente a un equipo con un gran nmero de procesadores. Sin embargo, como en todos los casos, se debe probar exhaustivamente la aplicacin para determinar el mejor enfoque.
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Parallel_File { internal class Program { private static void Main(string[] args) { TraverseTreeParallelForEach(@"C:\Program Files", (f) => { // For this demo we don't do anything with the data // except to read it. byte[] data = File.ReadAllBytes(f); // For user interest, although it slows down the operation. Console.WriteLine(f); }); // Keep the console window open. Console.ReadKey(); } public static void TraverseTreeParallelForEach(string root, Action<string> action) { //Count of files traversed and timer for diagnostic output int fileCount = 0; var sw = Stopwatch.StartNew();
// Use this value to determine whether to parallelize // file processing on each folder. int procCount = System.Environment.ProcessorCount; // Data structure to hold names of subfolders to be // examined for files. Stack<string> dirs = new Stack<string>(); if (!System.IO.Directory.Exists(root)) { throw new ArgumentException(); } dirs.Push(root); while (dirs.Count > 0) { string currentDir = dirs.Pop(); string[] subDirs = null; string[] files = null; try { subDirs = System.IO.Directory.GetDirectories(currentDir); } // An UnauthorizedAccessException exception will be thrown if we do not have // discovery permission on a folder or file. It may or may not be acceptable // to ignore the exception and continue enumerating the remaining files and // folders. It is also possible (but unlikely) that a DirectoryNotFound exception // will be raised. This will happen if currentDir has been deleted by // another application or thread after our call to Directory.Exists. The // choice of which exceptions to catch depends entirely on the specific task // you are intending to perform and also on how much you know with certainty // about the systems on which this code will run. catch (UnauthorizedAccessException e) { Console.WriteLine(e.Message); continue; } catch (System.IO.DirectoryNotFoundException e) { Console.WriteLine(e.Message); continue; } try { files = System.IO.Directory.GetFiles(currentDir); } catch (UnauthorizedAccessException e) { Console.WriteLine(e.Message); continue; } catch (System.IO.DirectoryNotFoundException e) { Console.WriteLine(e.Message); continue; } // Perform the required action on each file here in parallel if there are a sufficient number of files in the directory // or else sequentially if not. Files are opened and processed synchronously but this could be modified to perform async I/O. try { if (files.Length < procCount) { foreach (var file in files) { action(file); fileCount++; } } else { Parallel.ForEach(files, () => 0, (file, loopState, localCount) => { action(file); return (int) ++localCount; }, (c) => { Interlocked.Exchange(ref fileCount, fileCount + c); }); } } catch (AggregateException ae) { ae.Handle((ex) => { if (ex is UnauthorizedAccessException) { // Here we just output a message and go on. Console.WriteLine(ex.Message); return true;
} // Handle other exceptions here if necessary... return false; }); } // Push the subdirectories onto the stack for traversal. This could also be done before handing the files. foreach (string str in subDirs) dirs.Push(str); } // For diagnostic purposes. Console.WriteLine("Processed {0} files in {1} milleseconds", fileCount, sw.ElapsedMilliseconds); } } }
En este ejemplo, la E/S de archivo se realiza de forma sincrnica. Al trabajar con archivos grandes o conexiones de red lentas, puede ser preferible obtener acceso a los archivos de forma asincrnica. Puede combinar las tcnicas de E/S asincrnica con la iteracin paralela. Para obtener ms informacin, vea TPL y la programacin asincrnica tradicional de .NET. Tenga en cuenta que si se produce una excepcin en el subproceso principal, los subprocesos que inicia el mtodo ForEach podran seguir ejecutndose. Para detener estos subprocesos, puede establecer una variable booleana en los controladores de excepciones y comprobar su valor en cada iteracin del bucle paralelo. Si el valor indica que se ha iniciado una excepcin, use la variable ParallelLoopState para detener o interrumpir el bucle. la biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) se basa en el concepto de tarea ("task" en ingls). El trmino paralelismo de tareas hace referencia a la ejecucin simultnea de una o varias tareas independientes. Una tarea representa una operacin asincrnica y, en ciertos aspectos, se asemeja a la creacin de un nuevo subproceso o elemento de trabajo ThreadPool, pero con un nivel de abstraccin mayor. Las tareas proporcionan dos ventajas fundamentales:
Un uso ms eficaz y ms escalable de los recursos del sistema. En segundo plano, las tareas se ponen en la cola del elemento ThreadPool, que se ha mejorado con algoritmos (como el algoritmo de ascenso de colina o "hill-climbing") que determinan y ajustan el nmero de subprocesos con el que se maximiza el rendimiento. Esto hace que las tareas resulten relativamente ligeras y que, por tanto, pueda crearse un gran nmero de ellas para habilitar un paralelismo pormenorizado. Como complemento y para proporcionar el equilibrio de carga, se usan los conocidos algoritmos de robo de trabajo.
Un mayor control mediante programacin del que se puede conseguir con un subproceso o un elemento de trabajo. Las tareas y el marco que se crea en torno a ellas proporcionan un amplio conjunto de API que admiten el uso de esperas, cancelaciones, continuaciones, control robusto de excepciones, estado detallado, programacin personalizada, y ms.
El mtodo Parallel.Invoke proporciona una manera conveniente de ejecutar cualquier nmero de instrucciones arbitrarias simultneamente. Pase un delegado Action por cada elemento de trabajo. La manera ms fcil de crear estos delegados es con expresiones lambda. La expresin lambda puede llamar a un mtodo con nombre o proporcionar el cdigo alineado. En el siguiente ejemplo se muestra una llamada a Invoke bsica que crea e inicia dos tareas que se ejecutan a la vez. Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
Una tarea se representa mediante la clase System.Threading.Tasks.Task. Una tarea que devuelve un valor se representa mediante la clase System.Threading.Tasks.Task<TResult>, que se hereda de Task. El objeto de tarea administra los detalles de la infraestructura y proporciona mtodos y propiedades a los que se puede obtener acceso desde el subproceso que realiza la llamada a lo largo de la duracin de la tarea. Por ejemplo, se puede tener acceso a la propiedad Status de una tarea en cualquier momento para determinar si ha empezado a ejecutarse, si se ha ejecutado hasta su finalizacin, si se ha cancelado o si se ha producido una excepcin. El estado se representa mediante la enumeracin TaskStatus. Cuando se crea una tarea, se proporciona un delegado de usuario que encapsula el cdigo que la tarea va a ejecutar. El delegado se puede expresar como un delegado con nombre, un mtodo annimo o una expresin lambda. // Create a task and supply a user delegate by using a lambda expression. var taskA = new Task(() => Console.WriteLine("Hello from taskA.")); // Start the task. taskA.Start(); // Output a message from the joining thread. Console.WriteLine("Hello from the calling thread.");
Tambin se puede usar el mtodo StartNew para crear e iniciar una tarea en una sola operacin // Create and start the task in one operation. var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from taskA.")); // Output a message from the joining thread. Console.WriteLine("Hello from the joining thread."); La tarea expone una propiedad Factory esttica que devuelve una instancia predeterminada de TaskFactory, por lo que se puede llamar al mtodo como Task.Factory.StartNew(). Asimismo, en este ejemplo, dado que las tareas son de tipo System.Threading.Tasks.Task<TResult>, cada una tiene una propiedad Result pblica que contiene el resultado del clculo. Las tareas se ejecutan de forma asincrnica y pueden completarse en cualquier orden. Si se obtiene acceso a Result antes de que el clculo se complete, la propiedad se bloquear el subproceso hasta que el valor est disponible. Task<double>[] taskArray = new Task<double>[] { Task<double>.Factory.StartNew(() => DoComputation1()), // May be written more conveniently like this: Task.Factory.StartNew(() => DoComputation2()), Task.Factory.StartNew(() => DoComputation3()) }; double[] results = new double[taskArray.Length]; for (int i = 0; i < taskArray.Length; i++) results[i] = taskArray[i].Result; Cuando se usa una expresin lambda para crear el delegado de una tarea, se obtiene acceso a todas las variables que estn visibles en ese momento en el cdigo fuente. Sin embargo, en algunos casos, sobre todo en los bucles, una expresin lambda no captura la variable como cabra esperar. Captura solo el valor final, no el valor tal y como se transforma despus de cada iteracin. Puede obtener acceso al valor en cada iteracin si proporciona un objeto de estado a una tarea a travs de su constructor
class MyCustomData { public long CreationTime; public int Name; public int ThreadNum; } void TaskDemo2() { // Create the task object by using an Action(Of Object) to pass in custom data // in the Task constructor. This is useful when you need to capture outer variables // from within a loop. As an experiement, try modifying this code to // capture i directly in the lambda, and compare results. Task[] taskArray = new Task[10]; for(int i = 0; i < taskArray.Length; i++) { taskArray[i] = new Task((obj) => { MyCustomData mydata = (MyCustomData) obj; mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId; Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.", mydata.Name, mydata.CreationTime, mydata.ThreadNum) }, new MyCustomData () {Name = i, CreationTime = DateTime.Now.Ticks} ); taskArray[i].Start(); } } Este estado se pasa como argumento al delegado de la tarea y es accesible desde el objeto de tarea mediante la propiedad AsyncState. Adems, el paso de los datos a travs del constructor podra proporcionar una pequea ventaja de rendimiento en algunos escenarios.
Cada tarea recibe un identificador entero que la identifica de manera inequvoca en un dominio de aplicacin y al que se puede obtener acceso mediante la propiedad Id. El identificador resulta til para ver informacin sobre la tarea en las ventanas Pilas paralelas y Tareas paralelas del depurador de Visual Studio. El identificador se crea de forma diferida, lo que significa que no se crea hasta que se solicita; por tanto, una tarea podr tener un identificador diferente cada vez que se ejecute el programa. La mayora de las API que crean tareas proporcionan sobrecargas que aceptan un parmetro TaskCreationOptions. Al especificar una de estas opciones, se le est indicando al programador cmo se programa la tarea en el grupo de subprocesos. En la tabla siguiente se muestran las diversas opciones de creacin de tareas. Elemento None Descripcin Es la opcin predeterminada si no se especifica ninguna opcin. El programador usa su heurstica predeterminada para programar la tarea. PreferFairness Especifica que la tarea debe programarse de modo que las tareas creadas anteriormente tengan ms posibilidades de ejecutarse antes y que las tareas posteriormente tengan ms posibilidades de ejecutarse despus. LongRunning Especifica que la tarea representa una operacin de ejecucin prolongada. AttachedToParent Especifica que una tarea debe crearse como un elemento secundario asociado de la tarea actual, si existe. var task3 = new Task(() => MyLongRunningMethod(), TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness); task3.Start(); El mtodo Task.ContinueWith y el mtodo Task<TResult>.ContinueWith permiten especificar que una tarea se inicie cuando la tarea anterior se complete. Al delegado de la tarea de continuacin se le pasa una referencia de la tarea anterior para que pueda examinar su estado. Adems, la tarea de continuacin puede recibir de la tarea anterior un valor definido por el usuario en la propiedad Result para que la salida de la tarea anterior pueda servir de entrada de la tarea de continuacin. En el ejemplo siguiente, el cdigo del programa inicia getData; a continuacin, se inicia analyzeData automticamente cuando getData se completa; por ltimo, reportData se inicia cuando analyzeData se completa. getData genera como resultado una matriz de bytes, que se pasa a analyzeData. analyzeData procesa esa matriz y devuelve un resultado cuyo tipo se infiere del tipo devuelto del mtodo Analyze. reportData toma la entrada de analyzeData y genera un resultado cuyo tipo se infiere de forma similar y que se pasa a estar disponible en el programa en la propiedad Result. Task<byte[]> getData = new Task<byte[]>(() => GetFileData()); Task<double[]> analyzeData = getData.ContinueWith(x => Analyze(x.Result)); Task<string> reportData = analyzeData.ContinueWith(y => Summarize(y.Result)); getData.Start(); //or... Task<string> reportData2 = Task.Factory.StartNew(() => GetFileData()) .ContinueWith((x) => Analyze(x.Result)) .ContinueWith((y) => Summarize(y.Result)); System.IO.File.WriteAllText(@"C:\reportFolder\report.txt", reportData.Result); Los mtodos ContinueWhenAll y ContinueWhenAny permiten continuar a partir de varias tareas. Cuando el cdigo de usuario que se est ejecutando en una tarea crea una nueva tarea y no especifica la opcin AttachedToParent, la nueva tarea no se sincroniza con la tarea externa de ninguna manera especial. Este tipo de tareas se denominan tareas anidadas desasociadas. var outer = Task.Factory.StartNew(() => { Console.WriteLine("Outer task beginning."); var child = Task.Factory.StartNew(() => { Thread.SpinWait(5000000); Console.WriteLine("Detached task completed."); }); }); outer.Wait(); Console.WriteLine("Outer task completed."); /* Output: Outer task beginning. Outer task completed. Detached task completed.
*/ Cuando el cdigo de usuario que se est ejecutando en una tarea crea una tarea con la opcin AttachedToParent, la nueva tarea se concibe como una tarea secundaria de la tarea original, que se denomina tarea primaria. Puede usar la opcin AttachedToParent para expresar el paralelismo de tareas estructurado, ya que la tarea primaria espera implcitamente a que todas las tareas secundarias se completen. var parent = Task.Factory.StartNew(() => { Console.WriteLine("Parent task beginning."); var child = Task.Factory.StartNew(() => { Thread.SpinWait(5000000); Console.WriteLine("Attached child completed."); }, TaskCreationOptions.AttachedToParent); }); parent.Wait(); Console.WriteLine("Parent task completed."); /* Output: Parent task beginning. Attached task completed. Parent task completed. */
El tipo System.Threading.Tasks.Task y el tipo System.Threading.Tasks.Task<TResult> proporcionan varias sobrecargas de un mtodo Task.Wait y Task<TResult>.Wait que permiten esperar a que una tarea se complete. Adems, las sobrecargas del mtodo Task.WaitAll esttico y del mtodo Task.WaitAny permiten esperar a que se complete alguna o todas las tareas de una matriz de tareas. Normalmente, una tarea se espera por una de estas razones:
El subproceso principal depende del resultado final que se calcula mediante una tarea. Hay que controlar las excepciones que pueden producirse en la tarea.
Task[] tasks = new Task[3] { Task.Factory.StartNew(() => MethodA()), Task.Factory.StartNew(() => MethodB()), Task.Factory.StartNew(() => MethodC()) }; //Block until all tasks complete. Task.WaitAll(tasks);
Algunas sobrecargas permiten especificar un tiempo de espera, mientras que otras toman un objeto CancellationToken adicional como parmetro de entrada, de modo que la espera puede cancelarse mediante programacin o en respuesta a los datos proporcionados por el usuario. Cuando se espera a una tarea, se espera implcitamente a todos los elementos secundarios de esa tarea que se crearon con la opcin AttachedToParent de TaskCreationOptions. Task.Wait devuelve un valor inmediatamente si la tarea ya se ha completado. Un mtodo Wait producir las tareas generadas por una tarea incluso si se llama a este mtodo Wait una vez completada la tarea. Cuando una tarea produce una o varias excepciones, las excepciones se encapsulan en un objeto AggregateException. Esa excepcin se propaga de nuevo al subproceso que se combina con la tarea, que normalmente es el subproceso que est esperando a la tarea o que intenta tener acceso a la propiedad Result de la tarea. Este comportamiento sirve para aplicar la directiva de .NET Framework por la que, de manera predeterminada, todas las excepciones no controladas deben anular el proceso. El cdigo de llamada puede controlar las excepciones a travs de los mtodos Wait, WaitAll o WaitAny o de la propiedad Result() de la tarea o grupo de tareas, mientras incluye el mtodo Wait en un bloque try-catch.
El subproceso de unin tambin puede controlar excepciones; para ello, obtiene acceso a la propiedad Exception antes de que la tarea se recolecte como elemento no utilizado. Al obtener acceso a esta propiedad, impide que la excepcin no controlada desencadene el comportamiento de propagacin de la excepcin que anula el proceso cuando el objeto ha finalizado. La clase Task admite la cancelacin cooperativa y su completa integracin con las clases System.Threading.CancellationTokenSource y System.Threading.CancellationToken, que son nuevas en .NET Framework versin 4. Muchos de los constructores de la clase System.Threading.Tasks.Task toman un objeto CancellationToken como parmetro de entrada. Muchas de las sobrecargas de StartNew toman tambin CancellationToken. Puede crear el token y emitir la solicitud de cancelacin posteriormente usando la clase CancellationTokenSource. A continuacin, debe pasar el token a Task como argumento y hacer referencia al mismo token tambin en el delegado de usuario, que se encarga de responder a una solicitud de cancelacin. La clase TaskFactory proporciona mtodos estticos que encapsulan algunos modelos comunes de creacin e inicio de tareas y tareas de continuacin.
El modelo ms comn es StartNew, que crea e inicia una tarea en una sola instruccin. Para obtener ms informacin, vea StartNew(). Cuando cree tareas de continuacin a partir de varios antecedentes, use el mtodo ContinueWhenAll o el mtodoContinueWhenAnyo sus equivalentes en la clase Task<TResult>. Para obtener ms informacin, vea Tareas de continuacin. Para encapsular los mtodos BeginX y EndX del modelo de programacin asincrnica en una instancia de Task o Task<TResult>, use los mtodos FromAsync. Para obtener ms informacin, vea TPL y la programacin asincrnica tradicional de .NET.
El objeto TaskFactory predeterminado es accesible como propiedad esttica de la clase Task o de la clase Task<TResult>. Tambin pueden crearse directamente instancias de TaskFactory y especificar varias opciones entre las que se incluyan las opciones CancellationToken, TaskCreationOptions, TaskContinuationOptions o TaskScheduler. Cualquier opcin que se especifique al crear el generador de tareas se aplicar a todas las tareas que este generador cree, a menos que la tarea se cree usando la enumeracin TaskCreationOptions, en cuyo caso las opciones de la tarea reemplazarn a las del generador de tareas. En algunos casos, es posible que desee usar un objeto Task para encapsular alguna operacin asincrnica ejecutada por un componente externo en lugar de su propio usuario delegado. Si la operacin se basa en el patrn Begin/End del modelo de programacin asincrnica, puede usar los mtodos FromAsync. Si no es este el caso, puede usar el objeto TaskCompletionSource<TResult> para encapsular la operacin en una tarea y, de este modo, aprovechar algunas de las ventajas de programacin de Task, como por ejemplo, su compatibilidad con la propagacin de excepciones y el uso de continuaciones. La mayora de los desarrolladores de aplicaciones o bibliotecas no prestan atencin al procesador en el que se ejecuta la tarea, al modo en que la tarea sincroniza su trabajo con otras tareas o al modo en que se programa la tarea en el objeto System.Threading.ThreadPool. Solo necesitan que la ejecucin en el equipo host sea lo ms eficaz posible. Si necesita tener un control ms minucioso sobre los detalles de programacin, la biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) permite configurar algunos valores del programador de tareas predeterminado e incluso permite proporcionar un programador personalizado. TPL tiene varios tipos pblicos nuevos que resultan tiles tanto en escenarios en paralelo como en escenarios secuenciales. Entre ellos, se incluyen diversas clases de colecciones multiproceso rpidas y escalables del espacio de nombres System.Collections.Concurrent y varios tipos nuevos de sincronizacin, como SemaphoreLock y System.Threading.ManualResetEventSlim, que resultan ms eficaces que sus predecesores en tipos concretos de cargas de trabajo. Otros tipos nuevos de .NET Framework versin 4, como System.Threading.Barrier y System.Threading.SpinLock, proporcionan una funcionalidad que no estaba disponible en versiones anteriores. Se recomienda no heredar de System.Threading.Tasks.Task ni de System.Threading.Tasks.Task<TResult>. En su lugar, use la propiedad AsyncState para asociar los datos adicionales o el estado con un objeto Task o Task<TResult>. Tambin puede usar mtodos de extensin para extender la funcionalidad de las clases Task y Task<TResult>. i debe heredar de Task o Task<TResult>, no puede usar las clases System.Threading.Tasks.TaskFactory, System.Threading.Tasks.TaskFactory<TResult> ni System.Threading.Tasks.TaskCompletionSource<TResult> para crear instancias del tipo de tarea personalizada porque estas clases solo crean objetos Task y Task<TResult>. Adems, no puede usar los mecanismos de continuacin de tarea proporcionados por Task, Task<TResult>, TaskFactory y TaskFactory<TResult> para crear instancias del tipo de tarea personalizada porque tambin estos mecanismos crean solo objetos Task y Task<TResult>. En la programacin asincrnica, es muy comn que una operacin asincrnica, cuando se completa, invoque una segunda operacin y le pase datos. Tradicionalmente, esto se haca utilizando mtodos de devolucin de llamada. En la biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas), las tareas de continuacin proporcionan la misma funcionalidad. Una tarea de continuacin (o, simplemente, una continuacin) es una tarea asincrnica invocada por otra tarea, que se denomina antecedente, cuando se completa el antecedente. Las continuaciones son relativamente fciles de usar, y no por ello dejan de ser eficaces y flexibles. Por ejemplo, puede:
Pasar datos del antecedente a la continuacin Especificar las condiciones precisas en las que se invocar o no la continuacin Cancelar una continuacin antes de iniciarse o, de manera cooperativa, mientras se est ejecutando
Proporcionar sugerencias sobre cmo se debera programar la continuacin Invocar varias continuaciones desde el mismo antecedente Invocar una continuacin cuando se completa una parte o la totalidad de los antecedentes Encadenar las continuaciones una tras otra hasta cualquier longitud arbitraria Usar una continuacin para controlar las excepciones producidas por el antecedente
Las continuaciones se crean con el mtodo Task.ContinueWith. En el siguiente ejemplo se muestra el modelo bsico (para mayor claridad, se omite el control de excepciones). // The antecedent task. Can also be created with Task.Factory.StartNew. Task<DayOfWeek> taskA = new Task<DayOfWeek>(() => DateTime.Today.DayOfWeek); // The continuation. Its delegate takes the antecedent task // as an argument and can return a different type. Task<string> continuation = taskA.ContinueWith((antecedent) => { return String.Format("Today is {0}.", antecedent.Result); }); // Start the antecedent. taskA.Start(); // Use the contuation's result. Console.WriteLine(continuation.Result); Tambin puede crear una continuacin de varias tareas que se ejecutar cuando una parte o la totalidad de las tareas de una matriz de tareas se haya completado Task<int>[] tasks = new Task<int>[2]; tasks[0] = new Task<int>(() => { // Do some work... return 34; }); tasks[1] = new Task<int>(() => { // Do some work... return 8; }); var continuation = Task.Factory.ContinueWhenAll( tasks, (antecedents) => { int answer = tasks[0].Result + tasks[1].Result; Console.WriteLine("The answer is {0}", answer); }); tasks[0].Start(); tasks[1].Start(); continuation.Wait();
Una continuacin se crea en el estado WaitingForActivation y, por lo tanto, nicamente puede iniciarla su tarea antecedente. Al llamar a Task.Start en una continuacin en el cdigo de usuario, se produce una excepcin System.InvalidOperationException. Una continuacin es un objeto Task y no bloquea el subproceso en el que se inicia. Use el mtodo Wait para bloquearlo hasta que la tarea de continuacin se complete. Elemento None Descripcin Especifica el comportamiento predeterminado cuando no se especifican TaskContinuationOptions. La continuacin se programar una vez completado el antecedente, independientemente del estado final de este. Si la tarea es una tarea secundaria, se crea como
OnlyOnCanceled ExecuteSynchronously
una tarea anidada desasociada. Especifica que la continuacin se programar de modo que las tareas programadas antes tengan ms posibilidades de ejecutarse antes y las tareas programadas despus tengan ms posibilidades de ejecutarse ms tarde. Especifica que la continuacin ser una operacin general de larga duracin. Proporciona una sugerencia al System.Threading.Tasks.TaskScheduler de que se puede garantizar la sobresuscripcin. Especifica que la continuacin, si es una tarea secundaria, se adjunta a un elemento primario en la jerarqua de tareas. La continuacin es una tarea secundaria solo si su antecedente tambin es una tarea secundaria. Especifica que no se debe programar la continuacin si su antecedente se ejecuta completamente. Especifica que no se debe programar la continuacin si su antecedente produjo una excepcin no controlada. Especifica que no se debe programar la continuacin si se cancela su antecedente. Especifica que la continuacin solo se debe programar si el antecedente se ejecuta completamente. Especifica que la continuacin solo se debe programar si su antecedente produjo una excepcin no controlada. Al usar la opcin OnlyOnFaulted, se garantiza que la propiedad Exception del antecedente no es NULL. Puede usar esa propiedad para detectar la excepcin y ver qu excepcin provoc el error de la tarea. Si no tiene acceso a la propiedad Exception, no se controlar la excepcin. Asimismo, si intenta tener acceso a la propiedad Result de una tarea cancelada o con errores, se producir una nueva excepcin. Especifica que la continuacin debe programarse nicamente si su antecedente se completa en estado Canceled. Para las continuaciones de muy corta duracin. Especifica que lo ideal es que la continuacin se ejecute en el mismo subproceso que causa la transicin del antecedente a su estado final. Si el antecedente ya se ha completado cuando se crea la continuacin, el sistema intentar ejecutar la continuacin en el subproceso que la crea. Si se elimina CancellationTokenSource del antecedente en un bloque finally (Finally en Visual Basic), se ejecutar una continuacin con esta opcin en ese bloque finally.
Una referencia al antecedente se pasa como argumento al delegado de usuario de la continuacin. Si el antecedente es System.Threading.Tasks.Task<TResult> y la tarea se ejecut completamente, la continuacin puede tener acceso a la propiedad Task<TResult>.Result de la tarea. Con una continuacin de varias tareas y el mtodo Task.WaitAll, el argumento es la matriz de antecedentes. Al usar Task.WaitAny, el argumento es el primer antecedente que se complet. Task<TResult>.Result se bloquea hasta que la tarea se ha completado. Sin embargo, si la tarea se cancel o tiene errores, Result produce una excepcin cuando el cdigo intenta tener acceso al mismo. Puede evitar este problema mediante la opcin OnlyOnRanToCompletion var t = Task<int>.Factory.StartNew(() => 54); var c = t.ContinueWith((antecedent) => { Console.WriteLine("continuation {0}", antecedent.Result); }, TaskContinuationOptions.OnlyOnRanToCompletion); Si desea que la continuacin se ejecute aunque el antecedente no se ejecute completamente, debe usar medidas de proteccin contra la excepcin. Un posible enfoque es probar el estado del antecedente e intentar tener acceso a Result solamente si el estado no es Faulted o Canceled. Tambin puede examinar la propiedad Exception del antecedente. Una continuacin pasa al estado Canceled en estos escenarios:
Cuando produce una excepcin OperationCanceledException en respuesta a una solicitud de cancelacin. Al igual que sucede con cualquier tarea, si la excepcin contiene el mismo token que se pas a la continuacin, se trata como una confirmacin de cancelacin cooperativa. Cuando se pasa System.Threading.CancellationToken como argumento a la continuacin y la propiedad IsCancellationRequested del token es true (True) antes de ejecutar la continuacin. En este caso, la continuacin no se inicia y pasa directamente al estado Canceled. Cuando la continuacin nunca se ejecuta porque no se cumple la condicin establecida en TaskContinuationOptions. Por ejemplo, si una tarea entra en estado Faulted, su continuacin, creada con la opcin NotOnFaulted, pasar al estado Canceled y no se ejecutar.
Para que una continuacin no se ejecute si su antecedente se cancela, especifique la opcin NotOnCanceled al crear la continuacin. Si una tarea y su continuacin representan dos partes de la misma operacin lgica, puede pasar el mismo token de cancelacin a ambas tareas Task task = new Task(() => { CancellationToken ct = cts.Token; while (someCondition) { ct.ThrowIfCancellationRequested(); // Do the work. //...
} }, cts.Token ); Task task2 = task.ContinueWith((antecedent) => { CancellationToken ct = cts.Token; while (someCondition) { ct.ThrowIfCancellationRequested(); // Do the work. //... } }, cts.Token); task.Start(); //... // Antecedent and/or continuation will // respond to this request, depending on when it is made. cts.Cancel();
Si el antecedente no se cancela, todava se puede usar el token para cancelar la continuacin. Si el antecedente se cancela, no se inicia la continuacin. Despus de que una continuacin entra en estado Canceled, puede afectar a las continuaciones posteriores, dependiendo de las opciones TaskContinuationOptions especificadas para esas continuaciones. Las continuaciones eliminadas no se inician. Una continuacin no se ejecuta hasta que se completan su antecedente y todas las tareas secundarias asociadas. La continuacin no espera a que se completen las tareas secundarias desasociadas. El estado final de la tarea antecedente depende del estado final de cualquier tarea secundaria asociada. El estado de las tareas secundarias desasociadas no afecta a la tarea primaria. Una relacin entre un antecedente y una continuacin no es una relacin primario-secundario. Las excepciones producidas por las continuaciones no se propagan al antecedente. Por consiguiente, las excepciones que producen las continuaciones se deben controlar de igual modo que en cualquier otra tarea, como se indica a continuacin. 1. Use el mtodo Wait, WaitAny o WaitAll, o su homlogo genrico, para esperar en la continuacin. Puede esperar a un antecedente y sus continuaciones en la misma instruccin try
var t = Task<int>.Factory.StartNew(() => 54); var c = t.ContinueWith((antecedent) => { Console.WriteLine("continuation {0}", antecedent.Result); throw new InvalidOperationException(); }); try { t.Wait(); c.Wait(); } catch (AggregateException ae) { foreach(var e in ae.InnerExceptions) Console.WriteLine(e.Message); } Console.WriteLine("Exception handled. Let's move on.");
Use una segunda continuacin para observar la propiedad Exception de la primera continuacin. Si la continuacin es una tarea secundaria creada mediante la opcin AttachedToParent, la tarea primaria propagar sus excepciones al subproceso que realiza la llamada, como sucede con cualquier otro elemento secundario asociado. Una tarea anidada no es ms que una instancia de Task que se crea en el delegado de usuario de otra tarea. Una tarea secundaria es una tarea anidada que se crea con la opcin AttachedToParent. Una tarea puede crear cualquier nmero de tareas secundarias y anidadas, con la nica limitacin de los recursos del sistema. static void SimpleNestedTask() { var parent = Task.Factory.StartNew(() => { Console.WriteLine("Outer task executing."); var child = Task.Factory.StartNew(() => { Console.WriteLine("Nested task starting."); Thread.SpinWait(500000); Console.WriteLine("Nested task completing."); }); }); parent.Wait(); Console.WriteLine("Outer has completed."); } /* Sample output: Outer task executing. Nested task starting. Outer has completed. Nested task completing. */ El aspecto ms importante en lo que se refiere a las tareas secundarias y anidadas es que las tareas anidadas son esencialmente independientes de la tarea primaria o externa, mientras que las tareas secundarias asociadas estn estrechamente sincronizadas con el la tarea primaria. Si se modifica la instruccin de creacin de la tarea para usar la opcin AttachedToParent var child = Task.Factory.StartNew((t) => { Console.WriteLine("Attached child starting."); Thread.SpinWait(5000000); Console.WriteLine("Attached child completing."); }, TaskCreationOptions.AttachedToParent); /* Sample output: Parent task executing. Attached child starting. Attached child completing. Parent has completed. */ Puede usar tareas secundarias asociadas para crear grficos de operaciones asincrnicas con una estrecha sincronizacin. Sin embargo, en la mayora de los escenarios, recomendamos usar tareas anidadas porque las relaciones con otras tareas son menos complejas. Esta es la razn por la que las tareas que se crean dentro de otras tareas estn anidadas de forma predeterminada y es necesario especificar explcitamente la opcin AttachedToParent para crear una tarea secundaria. En la tabla siguiente se muestran las diferencias bsicas entre los dos tipos de tareas secundarias. Categora La tarea externa (primaria) espera a que las tareas internas se completen. La tarea primaria propaga las excepciones iniciadas por las tareas secundarias (tareas internas). El estado de la tarea primaria (tarea externa) depende del estado de la tarea secundaria (tarea interna). Tareas anidadas No No No Tareas secundarias asociadas S S S
En escenarios desasociados en los que la tarea anidada es un objetoTask<TResult>, se puede forzar que la tarea primaria espere a la secundaria mediante el acceso a la propiedad Result de la tarea anidada. La propiedad Result se bloquea hasta que su tarea se completa. static void WaitForSimpleNestedTask() { var outer = Task<int>.Factory.StartNew(() => { Console.WriteLine("Outer task executing."); var nested = Task<int>.Factory.StartNew(() => { Console.WriteLine("Nested task starting."); Thread.SpinWait(5000000); Console.WriteLine("Nested task completing."); return 42; }); // Parent will wait for this detached child. return nested.Result; }); Console.WriteLine("Outer has returned {0}.", outer.Result); } /* Sample output: Outer task executing. Nested task starting. Nested task completing. Outer has returned 42. */
Si una tarea anidada produce una excepcin, debe observarse o controlarse directamente en la tarea exterior como si se tratara de una tarea no anidada. Si una tarea secundaria adjunta inicia una excepcin, la excepcin se propaga automticamente a la tarea primaria y de nuevo al subproceso, que espera o intenta obtener acceso a la propiedad Result de la tarea. Por tanto, si se usan tareas secundarias asociadas, se pueden controlar todas las excepciones en un solo punto: la llamada a Wait del subproceso que realiza la llamada. No conviene olvidar que la cancelacin de tareas es cooperativa. Por tanto, para ser "cancelable", cada tarea secundaria asociada o desasociada debe supervisar el estado del token de cancelacin. Si desea cancelar un elemento primario y todos sus elementos secundarios utilizando una sola solicitud de cancelacin, debe pasar el mismo token como argumento a todas las tareas y proporcionar en cada tarea la lgica de respuesta a la solicitud. Si una tarea primaria se cancela antes de que se inicie una tarea secundaria, como es lgico, la tarea secundaria (anidada) nunca se cancelar. Si una tarea primaria se cancela despus de que se ha iniciado una tarea secundaria o anidada, la tarea anidada (secundaria) se ejecutar hasta completarse a menos que tenga su propia lgica de cancelacin. Si una tarea secundaria desasociada se cancela usando el mismo token que se pas a la tarea y la tarea primaria no espera a la secundaria, no se propagar ninguna excepcin, pues la excepcin se trata cono una cancelacin de cooperacin benigna. Este comportamiento es igual que el de cualquier tarea de nivel superior. Cuando una tarea secundaria asociada se cancela usando el mismo token que se pas a la tarea, se propaga una excepcin TaskCanceledException al subproceso de unin dentro de AggregateException. Es muy importante esperar a la tarea primaria para poder controlar todas las excepciones benignas adems de todos las excepciones de error que se propagan de manera ascendente a travs de un grfico de tareas secundarias asociadas.
Cancelacin de tareas Las clases System.Threading.Tasks.Task<TResult> y System.Threading.Tasks.Task admiten la cancelacin a travs del uso de tokens de cancelacin, que son una novedad de .NET Framework 4. Para obtener ms informacin, vea Cancelacin. En las clases de tareas, la cancelacin implica la cooperacin entre el delegado de usuario, que representa una operacin cancelable y el cdigo que solicit la cancelacin. Una cancelacin correcta implica que el cdigo solicitante llame al mtodo CancellationTokenSource.Cancel y que el delegado de usuario termine la operacin en el tiempo esperado. Puede finalizar la operacin a travs de una de estas opciones:
Devolver simplemente un valor del delegado. En muchos escenarios esto es suficiente; sin embargo, una instancia de tarea "cancelada" de esta manera cambia al estado RanToCompletion, no al estado Canceled. Producir una excepcin OperationCanceledException y pasarle el token en el que se solicit la cancelacin. En este caso, se prefiere usar el mtodo ThrowIfCancellationRequested. Una tarea cancelada de esta manera cambia al estado Canceled, que sirve al cdigo que realiza la llamada para comprobar que la tarea respondi a su solicitud de cancelacin.
using System; using System.Threading; using System.Threading.Tasks; internal class Cancel_Task { private static void Main() { var tokenSource2 = new CancellationTokenSource(); CancellationToken ct = tokenSource2.Token; var task = Task.Factory.StartNew(() => { // Were we already canceled? ct.ThrowIfCancellationRequested(); bool moreToDo = true; while (moreToDo) { // Poll on this property if you have to do // other cleanup before throwing. if (ct.IsCancellationRequested) { // Clean up here, then... ct.ThrowIfCancellationRequested(); } } }, tokenSource2.Token); // Pass same token to StartNew. tokenSource2.Cancel(); // Just continue on this thread, or Wait/WaitAll with try-catch: try { task.Wait(); } catch (AggregateException e) { foreach (var v in e.InnerExceptions) Console.WriteLine(e.Message + " " + v.Message); } Console.ReadKey(); } }
Cuando una instancia de tarea observa una excepcin OperationCanceledException iniciada desde el cdigo de usuario, compara el token de la excepcin con su token asociado (el que se pas a la API que cre la tarea). Si son iguales y la propiedad IsCancellationRequested del token devuelve true, la tarea lo interpreta como una confirmacin de cancelacin y pasa al estado Canceled. Si no se usa un mtodo WaitAll o Wait para esperar a la tarea, esta simplemente establece su estado en Canceled. Si espera en una tarea que cambia al estado Canceled, se crea y se inicia una excepcin TaskCanceledException (encapsulada en AggregateException). Observe que esta excepcin indica la cancelacin correcta en lugar de una situacin de error. Por consiguiente, la propiedad Exception de la tarea devuelve Null. Si la propiedad IsCancellationRequested del token devuelve False o si el token de la excepcin no coincide con el token de la tarea, OperationCanceledException se trata como una excepcin normal, por lo que la tarea cambia al estado Faulted. Observe tambin que la presencia de otras excepciones tambin har que la tarea pase al estado Faulted. Puede obtener el estado de la tarea completada en la propiedad Status. Es posible que una tarea contine procesando algunos elementos una vez solicitada la cancelacin. Control de excepciones (Task Parallel Library) Las excepciones no controladas que se inician mediante el cdigo de usuario que se ejecuta dentro de una tarea se propagan de nuevo al subproceso de unin, excepto en determinados escenarios que se describen posteriormente en este tema. Las excepciones se propagan cuando se usa uno de los mtodos estticos o de instancia Task.Wait o Task<TResult>.Wait, y estos mtodos se controlan si la llamada se enmarca en una instruccin try-catch. Si una tarea es la tarea primaria de unas tareas secundarias asociadas o si se esperan varias tareas, pueden producirse varias excepciones. Para propagar todas las excepciones de nuevo al subproceso que realiza la llamada, la infraestructura de la tarea las encapsula en una instancia de AggregateException. AggregateException tiene una propiedad InnerExceptions que se puede enumerar para examinar todas las excepciones originales que se generaron y controlar (o no) cada una de ellas de forma individual. Aunque solo se inicie una nica excepcin, se encapsular en un objeto AggregateException.
var task1 = Task.Factory.StartNew(() => { throw new MyCustomException("I'm bad, but not too bad!"); }); try { task1.Wait(); } catch (AggregateException ae) { // Assume we know what's going on with this particular exception. // Rethrow anything else. AggregateException.Handle provides // another way to express this. See later example. foreach (var e in ae.InnerExceptions) { if (e is MyCustomException) { Console.WriteLine(e.Message); } else { throw; } } }
Para evitar una excepcin no controlada, basta con detectar el objeto AggregateException y omitir las excepciones internas. Sin embargo, esta operacin no resulta recomendable porque es igual que detectar el tipo Exception base en escenarios no paralelos. Si desea detectar una excepcin sin realizar acciones concretas que la resuelvan, puede dejar al programa en un estado indeterminado. Si no espera que ninguna tarea propague la excepcin ni tiene acceso a su propiedad Exception, la excepcin se escalar conforme a la directiva de excepciones de .NET cuando la tarea se recopile como elemento no utilizado. Cuando las excepciones pueden propagarse de nuevo al subproceso de unin, es posible que una tarea contine procesando algunos elementos despus de que se haya producido la excepcin. Si una tarea tiene una tarea secundaria adjunta que inicia una excepcin, esa excepcin se encapsula en un objeto AggregateException antes de que se propague a la tarea primaria, que encapsula esa excepcin en su propio objeto AggregateException antes de propagarla de nuevo al subproceso que realiza la llamada. En casos como este, la propiedad AggregateException().InnerExceptions del objeto AggregateException que se detecta en el mtodo Task.Wait, Task<TResult>.Wait, WaitAny o WaitAll contiene una o varias instancias de AggregateException, pero no las excepciones originales que produjeron el error. Si desea evitar tener que iterar en los objetos anidados AggregateExceptions, puede usar el mtodo Flatten() para quitar todos los objetos anidados AggregateExceptions de forma que la propiedad AggregateException() InnerExceptions contenga las excepciones originales.
// task1 will throw an AE inside an AE inside an AE var task1 = Task.Factory.StartNew(() => { var child1 = Task.Factory.StartNew(() => { var child2 = Task.Factory.StartNew(() => { throw new MyCustomException("Attached child2 faulted."); }, TaskCreationOptions.AttachedToParent); // Uncomment this line to see the exception rethrown. // throw new MyCustomException("Attached child1 faulted."); }, TaskCreationOptions.AttachedToParent); }); try {
task1.Wait(); } catch (AggregateException ae) { foreach (var e in ae.Flatten().InnerExceptions) { if (e is MyCustomException) { // Recover from the exception. Here we just // print the message for demonstration purposes. Console.WriteLine(e.Message); } else { throw; } } // or ... // ae.Flatten().Handle((ex) => ex is MyCustomException); }
De forma predeterminada, las tareas secundarias estn desasociadas cuando se crean. Las excepciones producidas por tareas desasociadas deben controlarse o reiniciarse en la tarea primaria inmediata; no se propagan de nuevo al subproceso que realiza la llamada del mismo modo que las tareas secundarias asociadas. La tarea primaria superior puede reiniciar manualmente una excepcin de una tarea desasociada para encapsularla en un objeto AggregateException y propagarla de nuevo al subproceso de unin. var task1 = Task.Factory.StartNew(() => { var nested1 = Task.Factory.StartNew(() => { throw new MyCustomException("Nested task faulted."); }); // Here the exception will be escalated back to joining thread. // We could use try/catch here to prevent that. nested1.Wait(); }); try { task1.Wait(); } catch (AggregateException ae) { foreach (var e in ae.Flatten().InnerExceptions) { if (e is MyCustomException) { // Recover from the exception. Here we just // print the message for demonstration purposes. Console.WriteLine(e.Message); } } }
Aunque se use una tarea de continuacin para observar una excepcin en una tarea secundaria, la tarea primaria debe seguir observando la excepcin. Cuando el cdigo de usuario de una tarea responde a una solicitud de cancelacin, el procedimiento correcto es producir una excepcin OperationCanceledException que se pasa en el token de cancelacin con el que se comunic la solicitud. Antes de intentar propagar la excepcin, la instancia de la tarea compara el token de la excepcin con el que recibi durante su creacin. Si son iguales, la tarea propaga una excepcin TaskCanceledException
encapsulada en un elemento AggregateException y puede verse cuando se examinan las excepciones internas. Sin embargo, si el subproceso de unin no est esperando la tarea, no se propagar esta excepcin concreta. var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var task1 = Task.Factory.StartNew(() => { CancellationToken ct = token; while (someCondition) { // Do some work... Thread.SpinWait(50000); ct.ThrowIfCancellationRequested(); } }, token); // No waiting required. El mtodo Handle() puede usarse para filtrar excepciones que pueden tratarse como "controladas" sin necesidad de usar ninguna otra lgica. En el delegado de usuario que se proporciona a Handle(), se puede examinar el tipo de excepcin, su propiedad Message() o cualquier otra informacin sobre esta excepcin que permita determinar si es benigna. Las excepciones en las que el delegado devuelve false se reinician inmediatamente en una nueva instancia de AggregateException despus de que Handle() devuelve un valor. En el siguiente fragmento de cdigo se usa un bucle foreach sobre las excepciones internas. foreach (var e in ae.InnerExceptions) { if (e is MyCustomException) { Console.WriteLine(e.Message); } else { throw; } }
En el siguiente fragmento de cdigo se muestra el uso del mtodo Handle() con la misma funcin. ae.Handle((ex) => { return ex is MyCustomException; });
Si una tarea se completa con el estado Faulted, se puede examinar su propiedad Exception para detectar qu excepcin concreta produjo el error. Un mecanismo adecuado para observar la propiedad Exception es usar una continuacin que se ejecute solo si se produce un error en la tarea anterior
var task1 = Task.Factory.StartNew(() => { throw new MyCustomException("Task1 faulted."); }) .ContinueWith((t) => { Console.WriteLine("I have observed a {0}", t.Exception.InnerException.GetType().Name); }, TaskContinuationOptions.OnlyOnFaulted);
En una aplicacin real, el delegado de continuacin podra registrar informacin detallada sobre la excepcin y posiblemente generar nuevas tareas para recuperarse de la excepcin. En algunos escenarios (por ejemplo, cuando se hospedan complementos que no son de confianza), es posible que se produzcan numerosas excepciones benignas y que resulte demasiado difcil observarlas todas manualmente. En estos casos, se puede proceder a controlar el evento TaskScheduler.UnobservedTaskException. La instancia de System.Threading.Tasks.UnobservedTaskExceptionEventArgs que se pasa al controlador se puede utilizar para evitar que la excepcin no observada se propague de nuevo al subproceso de unin. Devolver un valor de una tarea
using System; using System.Linq; using System.Threading.Tasks; namespace Paralell_Practices { internal class ReturnValue_Task { private static void Main() { // Return a value type with a lambda expression Task<int> task1 = Task<int>.Factory.StartNew(() => 1); int i = task1.Result; // Return a named reference type with a multi-line statement lambda. Task<Test> task2 = Task<Test>.Factory.StartNew(() => { string s = ".NET"; double d = 4.0; return new Test {Name = s, Number = d}; }); Test test = task2.Result; // Return an array produced by a PLINQ query Task<string[]> task3 = Task<string[]>.Factory.StartNew(() => { string path = @"C:\users\public\pictures\"; string[] files = System.IO.Directory.GetFiles(path); var result = (from file in files.AsParallel() let info = new System.IO.FileInfo(file) where info.Extension == ".jpg" select file).ToArray(); return result; }); foreach (var name in task3.Result) Console.WriteLine(name); } private class Test { public string Name { get; set; } public double Number { get; set; } } } }
La propiedad Result bloquea el subproceso que realiza la llamada hasta que la tarea finaliza. Esperar a que una o varias tareas se completen
using System; using System.Threading; using System.Threading.Tasks; namespace Paralell_Practices { internal class WaitFinally_Task { private static Random rand = new Random(); private static void Main(string[] args) { // Wait on a single task with no timeout specified. Task taskA = Task.Factory.StartNew(() => DoSomeWork(10000000)); taskA.Wait(); Console.WriteLine("taskA has completed."); // Wait on a single task with a timeout specified. Task taskB = Task.Factory.StartNew(() => DoSomeWork(10000000)); taskB.Wait(100); //Wait for 100 ms. if (taskB.IsCompleted) Console.WriteLine("taskB has completed."); else Console.WriteLine("Timed out before taskB completed."); // Wait for all tasks to complete. Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++) { tasks[i] = Task.Factory.StartNew(() => DoSomeWork(10000000)); } Task.WaitAll(tasks); // Wait for first task to complete. Task<double>[] tasks2 = new Task<double>[3]; // Try three different approaches to the problem. Take the first one. tasks2[0] = Task<double>.Factory.StartNew(() => TrySolution1()); tasks2[1] = Task<double>.Factory.StartNew(() => TrySolution2()); tasks2[2] = Task<double>.Factory.StartNew(() => TrySolution3()); int index = Task.WaitAny(tasks2); double d = tasks2[index].Result; Console.WriteLine("task[{0}] completed first with result of {1}.", index, d); Console.ReadKey(); } private static void DoSomeWork(int val) { // Pretend to do something. Thread.SpinWait(val); } private static double TrySolution1() { int i = rand.Next(1000000); // Simulate work by spinning Thread.SpinWait(i); return DateTime.Now.Millisecond; } private static double TrySolution2() { int i = rand.Next(1000000); // Simulate work by spinning Thread.SpinWait(i); return DateTime.Now.Millisecond; } private static double TrySolution3() { int i = rand.Next(1000000); // Simulate work by spinning Thread.SpinWait(i); Thread.SpinWait(1000000); return DateTime.Now.Millisecond; } } }
Por razones de simplicidad, estos ejemplos no muestran el cdigo de control de excepciones ni el cdigo de cancelacin. En la mayora de los casos, debe incluir un mtodo Wait en un bloque try-catch, porque la espera es el mecanismo por el que el cdigo de programa controla las excepciones que se inician en cualquiera de las tareas. Si la tarea se puede cancelar, debe comprobar las propiedades IsCanceled o IsCancellationRequested() antes de intentar utilizar la tarea o su propiedad Result(). Cancelar una tarea y sus elementos secundarios El subproceso que realiza la llamada no finaliza la tarea forzosamente, sino que solo seala que se solicita la cancelacin. Si la tarea ya se est ejecutando, es el delegado de usuario el que debe observar la solicitud y responder segn corresponda. Si la cancelacin se solicita antes de ejecutarse la tarea, el delegado de usuario nunca se ejecuta y el objeto de tarea pasa al estado Cancelado. En este ejemplo se muestra cmo finalizar un objeto Task y sus elementos secundarios en respuesta a una solicitud de cancelacin. Tambin se muestra que, cuando un delegado de usuario finaliza con una excepcin OperationCanceledException, el subproceso que realiza la llamada puede usar opcionalmente el mtodo Wait o el mtodo WaitAll para esperar a que las tareas finalicen. En este caso, el delegado debe usar un bloque try-catch para controlar las excepciones en el subproceso que realiza la llamada.
using System; using System.Threading; using System.Threading.Tasks; namespace Paralell_Practices { internal class CancellationWithOCE { private static void Main(string[] args) { Console.WriteLine("Press any key to start. Press 'c' to cancel."); Console.ReadKey(); var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token;
// Store references to the tasks so that we can wait on them and // observe their status after cancellation. Task[] tasks = new Task[10]; // Request cancellation of a single task when the token source is canceled. // Pass the token to the user delegate, and also to the task so it can // handle the exception correctly. tasks[0] = Task.Factory.StartNew(() => DoSomeWork(1, token), token); // Request cancellation of a task and its children. Note the token is passed // to (1) the user delegate and (2) as the second argument to StartNew, so // that the task instance can correctly handle the OperationCanceledException. tasks[1] = Task.Factory.StartNew(() => { // Create some cancelable child tasks. for (int i = 2; i < 10; i++) { // For each child task, pass the same token // to each user delegate and to StartNew. tasks[i] = Task.Factory.StartNew(iteration => DoSomeWork((int) iteration, token), i, token); } // Passing the same token again to do work on the parent task. // All will be signaled by the call to tokenSource.Cancel below. DoSomeWork(2, token); }, token); // Give the tasks a second to start. Thread.Sleep(1000); // Request cancellation from the UI thread. if (Console.ReadKey().KeyChar == 'c') { tokenSource.Cancel(); Console.WriteLine("\nTask cancellation requested."); // Optional: Observe the change in the Status property on the task. // It is not necessary to wait on tasks that have canceled. However, // if you do wait, you must enclose the call in a try-catch block to // catch the OperationCanceledExceptions that are thrown. If you do // not wait, no OCE is thrown if the token that was passed to the // StartNew method is the same token that requested the cancellation. #region Optional_WaitOnTasksToComplete try { Task.WaitAll(tasks); } catch (AggregateException e) { // For demonstration purposes, show the OCE message. foreach (var v in e.InnerExceptions) Console.WriteLine("msg: " + v.Message); } // Prove that the tasks are now all in a canceled state. for (int i = 0; i < tasks.Length; i++) Console.WriteLine("task[{0}] status is now {1}", i, tasks[i].Status); #endregion } // Keep the console window open while the // task completes its output. Console.ReadLine(); } private static void DoSomeWork(int taskNum, CancellationToken ct) { // Was cancellation already requested? if (ct.IsCancellationRequested) { Console.WriteLine("We were cancelled before we got started."); Console.WriteLine("Press Enter to quit."); ct.ThrowIfCancellationRequested(); } int maxIterations = 1000; // NOTE!!! A benign "OperationCanceledException was unhandled // by user code" error might be raised here. Press F5 to continue. Or, // to avoid the error, uncheck the "Enable Just My Code" // option under Tools > Options > Debugging. for (int i = 0; i < maxIterations; i++) { // Do a bit of work. Not too much. var sw = new SpinWait(); for (int j = 0; j < 3000; j++) sw.SpinOnce(); Console.WriteLine("...{0} ", taskNum); if (ct.IsCancellationRequested)
Controlar excepciones iniciadas por tareas En este primer ejemplo, se detecta la excepcin System.AggregateException y, a continuacin, se examina su propiedad AggregateExceptionInnerExceptions() para determinar si algunas de las excepciones se pueden controlar mediante el cdigo del programa. static string[] GetAllFiles(string str) { // Should throw an AccessDenied exception on Vista. return System.IO.Directory.GetFiles(str, "*.txt", System.IO.SearchOption.AllDirectories); } static void HandleExceptions() { // Assume this is a user-entered string. string path = @"C:\"; // Use this line to throw UnauthorizedAccessException, which we handle. Task<string[]> task1 = Task<string[]>.Factory.StartNew(() => GetAllFiles(path)); // Use this line to throw an exception that is not handled. // Task task1 = Task.Factory.StartNew(() => { throw new IndexOutOfRangeException(); } ); try { task1.Wait(); } catch (AggregateException ae) { ae.Handle((x) => { if (x is UnauthorizedAccessException) // This we know how to handle. { Console.WriteLine("You do not have permission to access all folders in this path."); Console.WriteLine("See your network administrator or try another path."); return true; } return false; // Let anything else stop the application. }); } Console.WriteLine("task1 has completed."); }
En este ejemplo, se detecta la excepcin System.AggregateException, pero no se intenta controlar ninguna de sus excepciones internas. En su lugar, se utiliza el mtodo Flatten para extraer las excepciones internas de todas las instancias anidadas de AggregateException y volver a iniciar una sola excepcin AggregateException que contiene directamente todas las excepciones internas no controladas. Al reducir la excepcin, resulta ms fcil controlarla mediante cdigo de cliente. static string[] GetValidExtensions(string path) { if (path == @"C:\") throw new ArgumentException("The system root is not a valid path."); return new string[10]; } static void RethrowAllExceptions() {
Task<string[]>[] tasks = new Task<string[]>[3]; tasks[0] = Task<string[]>.Factory.StartNew(() => GetAllFiles(path)); tasks[1] = Task<string[]>.Factory.StartNew(() => GetValidExtensions(path)); tasks[2] = Task<string[]>.Factory.StartNew(() => new string[10]);
//int index = Task.WaitAny(tasks2); //double d = tasks2[index].Result; try { Task.WaitAll(tasks); } catch (AggregateException ae) { throw ae.Flatten(); } Console.WriteLine("task1 has completed."); }
Encadenar varias tareas con continuaciones En la biblioteca TPL, una tarea cuyo mtodo ContinueWith se invoca se denomina tarea antecedente y la tarea que se define en el mtodo ContinueWith se denomina continuacin. En este ejemplo se muestra cmo usar los mtodos ContinueWith y ContinueWith de las clases Task y Task<TResult> para especificar una tarea que se inicia despus de que finalice su tarea antecedente. Tambin muestra cmo especificar una continuacin que solo se ejecuta si la tarea antecedente se cancela. En estos ejemplos se muestra cmo continuar desde una tarea nica. Tambin puede crear una continuacin que se ejecute despus de que alguno o todos los grupos de tareas se completen o se cancelen. Para obtener ms informacin, vea TaskContinueWhenAll() y TaskContinueWhenAny(). En el mtodo DoSimpleContinuation, se muestra la sintaxis bsica de ContinueWith. Observe que la tarea antecedente se proporciona como el parmetro de entrada a la expresin lambda en el mtodo ContinueWith. Esto le permite evaluar el estado de la tarea antecedente antes de realizar cualquier trabajo en la continuacin. Utilice esta sobrecarga simple de ContinueWith cuando no tenga que pasar un estado de una tarea a otra. En el mtodo DoSimpleContinuationWithState, se muestra cmo utilizar ContinueWith para pasar el resultado de la tarea antecedente a la tarea de continuacin.
using System; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Paralell_Practices { internal class ContinueWith { private static void Main() { SimpleContinuation(); Console.WriteLine("Press any key to exit"); Console.ReadKey(); } private static void SimpleContinuation() { string path = @"C:\users\public\TPLTestFolder\"; try { var firstTask = new Task(() => CopyDataIntoTempFolder(path)); var secondTask = firstTask.ContinueWith((t) => CreateSummaryFile(path)); firstTask.Start(); } catch (AggregateException e) { Console.WriteLine(e.Message);
} } // A toy function to simulate a workload private static void CopyDataIntoTempFolder(string path) { System.IO.Directory.CreateDirectory(path); Random rand = new Random(); for (int x = 0; x < 50; x++) { byte[] bytes = new byte[1000]; rand.NextBytes(bytes); string filename = Path.GetRandomFileName(); string filepath = Path.Combine(path, filename); System.IO.File.WriteAllBytes(filepath, bytes); } } private static void CreateSummaryFile(string path) { string[] files = System.IO.Directory.GetFiles(path); Parallel.ForEach(files, (file) => { Thread.SpinWait(5000); }); System.IO.File.WriteAllText(Path.Combine(path, "__SummaryFile.txt"), "did my work"); Console.WriteLine("Done with task2"); } private static void SimpleContinuationWithState() { int[] nums = {19, 17, 21, 4, 13, 8, 12, 7, 3, 5}; var f0 = new Task<double>(() => nums.Average()); var f1 = f0.ContinueWith(t => GetStandardDeviation(nums, t.Result)); f0.Start(); Console.WriteLine("the standard deviation is {0}", f1.Result); } private static double GetStandardDeviation(int[] values, double mean) { double d = 0.0; foreach (var n in values) { d += Math.Pow(mean - n, 2); } return Math.Sqrt(d/(values.Length - 1)); } } }
El parmetro de tipo de Task<TResult> determina el tipo devuelto del delegado. Ese valor devuelto se pasa a la tarea de continuacin. Es posible encadenar un nmero arbitrario de tareas de esta manera. Recorrer un rbol binario con tareas paralelas public class TreeWalk { static void Main() { Tree<MyClass> tree = new Tree<MyClass>(); // ...populate tree (left as an exercise) // Define the Action to perform on each node. Action<MyClass> myAction = x => Console.WriteLine("{0} : {1}", x.Name, x.Number); // Traverse the tree with parallel tasks. DoTree(tree, myAction); } public class MyClass { public string Name { get; set; } public int Number { get; set; } } public class Tree<T> { public Tree<T> Left;
// By using tasks explcitly. public static void DoTree<T>(Tree<T> tree, Action<T> action) { if (tree == null) return; var left = Task.Factory.StartNew(() => DoTree(tree.Left, action)); var right = Task.Factory.StartNew(() => DoTree(tree.Right, action)); action(tree.Data); try { Task.WaitAll(left, right); } catch (AggregateException ) { //handle exceptions here } } // By using Parallel.Invoke public static void DoTree2<T>(Tree<T> tree, Action<T> action) { if (tree == null) return; Parallel.Invoke( () => DoTree2(tree.Left, action), () => DoTree2(tree.Right, action), () => action(tree.Data) ); } }
Los dos mtodos mostrados son equivalentes desde el punto de vista funcional. Cuando se usa el mtodo StartNew() para crear y ejecutar las tareas, estas devuelven un identificador que se puede usar para esperar en ellas y controlar las excepciones. TPL con otros modelos asincrnicos La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) se puede usar con modelos tradicionales de programacin asincrnica de .NET de varias maneras. TPL y la programacin asincrnica tradicional de .NET .NET Framework proporciona los siguientes dos modelos estndar para realizar las operaciones asincrnicas enlazadas a E/S y enlazadas a clculos:
Modelo de programacin asincrnica (APM), en el que las operaciones asincrnicas se representan mediante un par de mtodos Begin/End como FileStream.BeginRead y Stream.EndRead. Modelo asincrnico basado en eventos (EAP), en el que las operaciones asincrnicas se representan mediante un par mtodo-evento que se denomina OperationNameAsync y OperationNameCompleted, por ejemplo, WebClient.DownloadStringAsync y WebClient.DownloadStringCompleted. (EAP apareci por primera vez en .NET Framework versin 2.0).
La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) se puede usar de varias maneras junto con cualquiera de los modelos asincrnicos. Puede exponer las operaciones de APM y EAP como tareas a los consumidores de la biblioteca o puede exponer los modelos de APM, pero usar objetos de tarea para implementarlos internamente. En ambos escenarios, al usar los objetos de tarea, puede simplificar el cdigo y aprovechar la siguiente funcionalidad til:
Registre las devoluciones de llamada, en el formulario de continuaciones de la tarea, en cualquier momento despus de que se haya iniciado la tarea.
Coordine varias operaciones que se ejecutan en respuesta a un mtodo Begin_, mediante los mtodos ContinueWhenAny, ContinueWhenAll, WaitAll o WaitAny. Encapsule las operaciones asincrnicas enlazadas a E/S y enlazadas a clculos en el mismo objeto de tarea. Supervise el estado del objeto de tarea. Calcule las referencias del estado una operacin para un objeto de tarea mediante TaskCompletionSource<TResult>.
Ajustar las operaciones de APM en una tarea Las clases System.Threading.Tasks.TaskFactory y System.Threading.Tasks.TaskFactory<TResult> proporcionan varias sobrecargas de los mtodosFromAsync yFromAsyncque permiten encapsular un par de mtodos Begin/End en una instancia de Task o de Task<TResult>. Las diversas sobrecargas hospedan cualquier par de mtodos de Begin/End que tenga entre cero y tres parmetros de entrada. Para los pares que tienen mtodos End que devuelven un valor (Function en Visual Basic), use los mtodos de TaskFactory<TResult>, que crean un objeto Task<TResult>. Para los mtodos End que devuelven un valor void (Sub en Visual Basic), use los mtodos de TaskFactory, que crean un objeto Task. En los pocos casos en los que el mtodo Begin tiene ms de tres parmetros o contiene parmetros out o ref, se proporcionan las sobrecargas FromAsync adicionales que encapsulan slo el mtodo End. En el ejemplo de cdigo siguiente se muestra la signatura para la sobrecarga FromAsync que coincide con los mtodos FileStream.BeginRead y FileStream.EndRead. Esta sobrecarga toma los tres parmetros de entrada siguientes. public Task<TResult> FromAsync<TArg1, TArg2, TArg3>( Func<TArg1, TArg2, TArg3, AsyncCallback, object, IAsyncResult> beginMethod, //BeginRead Func<IAsyncResult, TResult> endMethod, //EndRead TArg1 arg1, // the byte[] buffer TArg2 arg2, // the offset in arg1 at which to start writing data TArg3 arg3, // the maximum number of bytes to read object state // optional state information ) El primer parmetro es un delegado Func<T1, T2, T3, T4, T5, TResult> que coincide con la signatura del mtodo FileStream.BeginRead. El segundo parmetro es un delegado Func<T, TResult> que toma una interfaz IAsyncResult y devuelve TResult. Dado que EndRead devuelve un entero, el compilador deduce el tipo de TResult como Int32 y el tipo de la tarea como Task<Int32>. Los ltimos cuatro parmetros son idnticos a los del mtodo FileStream.BeginRead:
Bfer donde se van a almacenar los datos de archivo. Desplazamiento en el bfer donde deben comenzar a escribirse los datos. Cantidad mxima de datos que se van a leer del archivo. Un objeto opcional que almacena los datos de estado definidos por el usuario que se van a pasar a la devolucin de llamada.
Usar ContinueWith para la funcionalidad de devolucin de llamada Si necesita obtener acceso a los datos del archivo, en contraposicin a solo el nmero de bytes, el mtodo FromAsync no es suficiente. En su ligar, use Task<String>, cuya propiedad Result contiene los datos de archivo. Puede hacer si agrega una continuacin a la tarea original. La continuacin realiza el trabajo que normalmente realizara el delegado AsyncCallback. Se invoca cuando se completa el antecedente y se ha rellenado el bfer de datos. (El objeto FileStream se debera cerrar antes de devolver un valor). En el siguiente ejemplo se muestra cmo devolver un objeto Task<String> que encapsula el par BeginRead/EndRead de la clase FileStream. const int MAX_FILE_SIZE = 14000000; public static Task<string> GetFileStringAsync(string path) { FileInfo fi = new FileInfo(path); byte[] data = null; data = new byte[fi.Length]; FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, true); //Task<int> returns the number of bytes read Task<int> task = Task<int>.Factory.FromAsync( fs.BeginRead, fs.EndRead, data, 0, data.Length, null);
// It is possible to do other work here while waiting // for the antecedent task to complete. // ... // Add the continuation, which returns a Task<string>. return task.ContinueWith((antecedent) => { fs.Close(); // Result = "number of bytes read" (if we need it.) if (antecedent.Result < 100) { return "Data is too small to bother with."; } else { // If we did not receive the entire file, the end of the // data buffer will contain garbage. if (antecedent.Result < data.Length) Array.Resize(ref data, antecedent.Result); // Will be returned in the Result property of the Task<string> // at some future point after the asynchronous file I/O operation completes. return new UTF8Encoding().GetString(data); } }); } A continuacin, se puede llamar al mtodo de la forma siguiente. Task<string> t = GetFileStringAsync(path); // Do some other work: // ... try { Console.WriteLine(t.Result.Substring(0, 500)); } catch (AggregateException ae) { Console.WriteLine(ae.InnerException.Message); }
Proporcionar los datos de estado personalizados En las operaciones IAsyncResult tpicas, si el delegado AsyncCallback requiere algn dato de estado personalizado, tiene que pasarlo a travs del ltimo parmetro Begin para que los datos se puedan empaquetar en el objeto IAsyncResult que se pasar finalmente al mtodo de devolucin de llamada. Normalmente no se requiere esto cuando se usan los mtodos FromAsync. Si los datos personalizados son conocidos para la continuacin, se pueden capturar directamente en el delegado de continuacin. El siguiente ejemplo se parece el ejemplo anterior, pero en lugar de examinar la propiedad Result del antecedente, la continuacin examina los datos de estado personalizados que son directamente accesibles al delegado de usuario de la continuacin. public Task<string> GetFileStringAsync2(string path) { FileInfo fi = new FileInfo(path); byte[] data = new byte[fi.Length]; MyCustomState state = GetCustomState(); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, true); // We still pass null for the last parameter because // the state variable is visible to the continuation delegate. Task<int> task = Task<int>.Factory.FromAsync( fs.BeginRead, fs.EndRead, data, 0, data.Length, null); return task.ContinueWith((antecedent) => {
// It is safe to close the filestream now. fs.Close(); // Capture custom state data directly in the user delegate. // No need to pass it through the FromAsync method. if (state.StateData.Contains("New York, New York")) { return "Start spreading the news!"; } else { // If we did not receive the entire file, the end of the // data buffer will contain garbage. if (antecedent.Result < data.Length) Array.Resize(ref data, antecedent.Result); // Will be returned in the Result property of the Task<string> // at some future point after the asynchronous file I/O operation completes. return new UTF8Encoding().GetString(data); } }); }
Sincronizar varias tareas FromAsync Los mtodos estticos ContinueWhenAny y ContinueWhenAll proporcionan flexibilidad adicional cuando se usan junto con los mtodos FromAsync. El siguiente ejemplo muestra cmo iniciar varias operaciones asincrnicas de E/S y, a continuacin, espera a que todos ellas se completen antes de ejecutar la continuacin. public Task<string> GetMultiFileData(string[] filesToRead) { FileStream fs; Task<string>[] tasks = new Task<string>[filesToRead.Length]; byte[] fileData = null; for (int i = 0; i < filesToRead.Length; i++) { fileData = new byte[0x1000]; fs = new FileStream(filesToRead[i], FileMode.Open, FileAccess.Read, FileShare.Read, fileData.Length, true); // By adding the continuation here, the // Result of each task will be a string. tasks[i] = Task<int>.Factory.FromAsync( fs.BeginRead, fs.EndRead, fileData, 0, fileData.Length, null) .ContinueWith((antecedent) => { fs.Close(); // If we did not receive the entire file, the end of the // data buffer will contain garbage. if (antecedent.Result < fileData.Length) Array.Resize(ref fileData, antecedent.Result); // Will be returned in the Result property of the Task<string> // at some future point after the asynchronous file I/O operation completes. return new UTF8Encoding().GetString(fileData); }); } // Wait for all tasks to complete. return Task<string>.Factory.ContinueWhenAll(tasks, (data) => { // Propagate all exceptions and mark all faulted tasks as observed. Task.WaitAll(data); // Combine the results from all tasks.
StringBuilder sb = new StringBuilder(); foreach (var t in data) { sb.Append(t.Result); } // Final result to be returned eventually on the calling thread. return sb.ToString(); }); }
Tareas FromAsync solo para el mtodo End En los pocos casos en los que el mtodo Begin requiere ms de tres parmetros de entrada o tiene parmetros out o ref, puede usar las sobrecargas FromAsync, por ejemplo, TaskFactory<TResult>.FromAsync(IAsyncResult, Func<IAsyncResult, TResult>), que representa slo el mtodo End. Estos mtodos tambin se pueden usar en cualquier escenario en el que se pasa IAsyncResult y desea encapsularlo en una tarea. static Task<String> ReturnTaskFromAsyncResult() { IAsyncResult ar = DoSomethingAsynchronously(); Task<String> t = Task<string>.Factory.FromAsync(ar, _ => { return (string)ar.AsyncState; }); return t; } Iniciar y cancelar las tareas FromAsync La tarea devuelta por un mtodo FromAsync tiene un estado de WaitingForActivation y la iniciar el sistema en algn momento una vez creada la tarea. Si intenta llamar a Start en este tipo de tarea, se producir una excepcin. No puede cancelar una tarea FromAsync, porque las API subyacentes de .NET Framework admiten actualmente la cancelacin en curso del la E/S de archivo o red. Puede agregar la funcionalidad de cancelacin a un mtodo que encapsula una llamada FromAsync, pero slo puede responder a la cancelacin antes de que se llame a FromAsync o despus de completar (por ejemplo, en una tarea de continuacin). Algunas clases que admiten EAP, por ejemplo, WebClient, admiten la cancelacin y esa funcionalidad de cancelacin nativa se puede integrar mediante los tokens de cancelacin. Exponer las operaciones de EAP complejas como tareas La TPL no proporciona ningn mtodo diseado especficamente para encapsular una operacin asincrnica basada en eventos del mismo modo que la familia de mtodos FromAsync ajusta el modelo IAsyncResult. Sin embargo, TPL proporciona la clase System.Threading.Tasks.TaskCompletionSource<TResult>, que se puede usar para representar cualquier conjunto arbitrario de operaciones como Task<TResult>. Las operaciones pueden ser sincrnicas o asincrnicas y pueden ser enlazadas a E/S o enlazadas a clculo, o ambos. En el siguiente ejemplo se muestra cmo usar TaskCompletionSource<TResult> para exponer un conjunto de operaciones WebClient asincrnicas al cdigo de cliente como un objeto Task bsico. El mtodo permite escribir una matriz de direcciones URL de web y un trmino o nombre que se va a buscar y, a continuacin, devuelve el nmero de veces que aparece el trmino de bsqueda en cada sitio.
Task<string[]> GetWordCountsSimplified(string[] urls, string name, CancellationToken token) { TaskCompletionSource<string[]> tcs = new TaskCompletionSource<string[]>(); WebClient[] webClients = new WebClient[urls.Length]; object m_lock = new object(); int count = 0; List<string> results = new List<string>(); // If the user cancels the CancellationToken, then we can use the // WebClient's ability to cancel its own async operations. token.Register(() => { foreach (var wc in webClients) { if (wc != null) wc.CancelAsync(); } });
for (int i = 0; i < urls.Length; i++) { webClients[i] = new WebClient(); #region callback // Specify the callback for the DownloadStringCompleted // event that will be raised by this WebClient instance. webClients[i].DownloadStringCompleted += (obj, args) => { // Argument validation and exception handling omitted for brevity. // Split the string into an array of words, // then count the number of elements that match // the search term. string[] words = args.Result.Split(' '); string NAME = name.ToUpper(); int nameCount = (from word in words.AsParallel() where word.ToUpper().Contains(NAME) select word).Count(); // Associate the results with the url, and add new string to the array that // the underlying Task object will return in its Result property. results.Add(String.Format("{0} has {1} instances of {2}", args.UserState, nameCount, name)); // If this is the last async operation to complete, // then set the Result property on the underlying Task. lock (m_lock) { count++; if (count == urls.Length) { tcs.TrySetResult(results.ToArray()); } } }; #endregion // Call DownloadStringAsync for each URL. Uri address = null; address = new Uri(urls[i]); webClients[i].DownloadStringAsync(address, address); } // end for // Return the underlying Task. The client code // waits on the Result property, and handles exceptions // in the try-catch block there. return tcs.Task; }
Recuerde que TaskCompletionSource iniciar cualquier tarea creada por TaskCompletionSource<TResult> y, por consiguiente, el cdigo de usuario no debera llamar al mtodo Start en esa tarea. Implementar el modelo de APM usando las tareas En algunos escenarios, puede ser deseable exponer directamente el modelo IAsyncResult mediante pares de mtodos Begin/End en una API. Por ejemplo, quizs desee mantener la coherencia con las API existentes o puede haber automatizado herramientas que requieren este modelo. En tales casos, puede usar las tareas para simplificar la forma en que se implementa internamente el modelo de APM. En el siguiente ejemplo se muestra cmo usar las tareas para implementar un par de mtodos Begin/End de APM para un mtodo enlazado a clculo de ejecucin prolongada.
using System; using System.Threading; using System.Threading.Tasks; namespace Paralell_Practices { internal class Calculator { public IAsyncResult BeginCalculate(int decimalPlaces, AsyncCallback ac, object state) { Console.WriteLine("Calling BeginCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId); Task<string> f = Task<string>.Factory.StartNew(_ => Compute(decimalPlaces), state); if (ac != null) f.ContinueWith((res) => ac(f)); return f; } public string Compute(int numPlaces) { Console.WriteLine("Calling compute on thread {0}", Thread.CurrentThread.ManagedThreadId); // Simulating some heavy work.
Thread.SpinWait(500000000); // Actual implemenation left as exercise for the reader. // Several examples are available on the Web. return "3.14159265358979323846264338327950288"; } public string EndCalculate(IAsyncResult ar) { Console.WriteLine("Calling EndCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId); return ((Task<string>) ar).Result; } } public class CalculatorClient { private static int decimalPlaces = 12; public static void Main() { Calculator calc = new Calculator(); int places = 35; AsyncCallback callBack = new AsyncCallback(PrintResult); IAsyncResult ar = calc.BeginCalculate(places, callBack, calc); // Do some work on this thread while the calulator is busy. Console.WriteLine("Working..."); Thread.SpinWait(500000); Console.ReadLine(); } public static void PrintResult(IAsyncResult result) { Calculator c = (Calculator) result.AsyncState; string piString = c.EndCalculate(result); Console.WriteLine("Calling PrintResult on thread {0}; result = {1}", Thread.CurrentThread.ManagedThreadId, piString); } } }
Encapsular modelos de EAP en una tarea En el ejemplo siguiente, se muestra cmo exponer una secuencia arbitraria de operaciones asincrnicas de host del Protocolo de autenticacin extensible (EAP) como una sola tarea mediante TaskCompletionSource<TResult>. El ejemplo tambin muestra cmo usar CancellationToken para invocar los mtodos de cancelacin integrados en los objetos WebClient.
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; namespace Paralell_Practices { internal class WebDataDownloader { private static void Main() { WebDataDownloader downloader = new WebDataDownloader(); string[] addresses = { "http://www.msnbc.com", "http://www.yahoo.com", "http://www.nytimes.com", "http://www.washingtonpost.com", "http://www.latimes.com", "http://www.newsday.com" }; CancellationTokenSource cts = new CancellationTokenSource(); // Create a UI thread from which to cancel the entire operation Task.Factory.StartNew(() => { Console.WriteLine("Press c to cancel"); if (Console.ReadKey().KeyChar == 'c') cts.Cancel(); }); // Using a neutral search term that is sure to get some hits. Task<string[]> webTask = downloader.GetWordCounts(addresses, "the", cts.Token); // Do some other work here unless the method has already completed. if (!webTask.IsCompleted) { // Simulate some work. Thread.SpinWait(5000000); }
string[] results = null; try { results = webTask.Result; } catch (AggregateException e) { foreach (var ex in e.InnerExceptions) { OperationCanceledException oce = ex as OperationCanceledException; if (oce != null) { if (oce.CancellationToken == cts.Token) { Console.WriteLine("Operation canceled by user."); } } else Console.WriteLine(ex.Message); } } if (results != null) { foreach (var item in results) Console.WriteLine(item); } Console.ReadKey(); } private Task<string[]> GetWordCounts(string[] urls, string name, CancellationToken token) { TaskCompletionSource<string[]> tcs = new TaskCompletionSource<string[]>(); WebClient[] webClients = new WebClient[urls.Length]; // If the user cancels the CancellationToken, then we can use the // WebClient's ability to cancel its own async operations. token.Register(() => { foreach (var wc in webClients) { if (wc != null) wc.CancelAsync(); } }); object m_lock = new object(); int count = 0; List<string> results = new List<string>(); for (int i = 0; i < urls.Length; i++) { webClients[i] = new WebClient(); #region callback // Specify the callback for the DownloadStringCompleted // event that will be raised by this WebClient instance. webClients[i].DownloadStringCompleted += (obj, args) => { if (args.Cancelled == true) { tcs.TrySetCanceled(); return; } else if (args.Error != null) { // Pass through to the underlying Task // any exceptions thrown by the WebClient // during the asynchronous operation. tcs.TrySetException(args.Error); return; } else { // Split the string into an array of words, // then count the number of elements that match // the search term. string[] words = null; words = args.Result.Split(' '); string NAME = name.ToUpper(); int nameCount = (from word in words.AsParallel() where word.ToUpper().Contains(NAME) select word) .Count(); // Associate the results with the url, and add new string to the array that // the underlying Task object will return in its Result property. results.Add(String.Format("{0} has {1} instances of {2}", args.UserState, nameCount, name)); }
// If this is the last async operation to complete, // then set the Result property on the underlying Task. lock (m_lock) { count++; if (count == urls.Length) { tcs.TrySetResult(results.ToArray()); } } }; #endregion // Call DownloadStringAsync for each URL. Uri address = null; try { address = new Uri(urls[i]); // Pass the address, and also use it for the userToken // to identify the page when the delegate is invoked. webClients[i].DownloadStringAsync(address, address); } catch (UriFormatException ex) { // Abandon the entire operation if one url is malformed. // Other actions are possible here. tcs.TrySetException(ex); return tcs.Task; } } // Return the underlying Task. The client code // waits on the Result property, and handles exceptions // in the try-catch block there. return tcs.Task; } }
Problemas potenciales en el paralelismo de datos y tareas En muchos casos, Parallel.For y Parallel.ForEach pueden proporcionar mejoras de rendimiento significativas en comparacin con los bucles secuenciales normales. Sin embargo, el trabajo de paralelizar el bucle aporta una complejidad que puede llevar a problemas que, en cdigo secuencial, no son tan comunes o que no se producen en absoluto. En este tema se indican algunas prcticas que se deben evitar al escribir bucles paralelos. No se debe suponer que la ejecucin en paralelo es siempre ms rpida En algunos casos, un bucle paralelo se podra ejecutar ms lentamente que su equivalente secuencial. La regla bsica es que no es probable que los bucles en paralelo que tienen pocas iteraciones y delegados de usuario rpidos aumenten gran cosa la velocidad. Sin embargo, como son muchos los factores implicados en el rendimiento, recomendamos siempre medir los resultados reales. Evitar la escritura en ubicaciones de memoria compartidas En cdigo secuencial, no es raro leer o escribir en variables estticas o en campos de clase. Sin embargo, cuando varios subprocesos tienen acceso a estas variables de forma simultnea, hay grandes posibilidades de que se produzcan condiciones de carrera. Aunque se pueden usar bloqueos para sincronizar el acceso a la variable, el costo de la sincronizacin puede afectar negativamente al rendimiento. Por tanto, se recomienda evitar, o al menos limitar, el acceso al estado compartido en un bucle en paralelo en la medida de lo posible. La manera mejor de hacerlo es mediante las sobrecargas de Parallel.For y Parallel.ForEach que utilizan una variable System.Threading.ThreadLocal<T> para almacenar el estado local del subproceso durante la ejecucin del bucle. Para obtener ms informacin, vea Cmo: Escribir un bucle Parallel.For que tenga variables locales de subproceso y Cmo: Escribir un bucle Parallel.ForEach que tenga variables locales de subproceso. Evitar la paralelizacin excesiva Si usa bucles en paralelo, incurrir en costos de sobrecarga al crear particiones de la coleccin de origen y sincronizar los subprocesos de trabajo. El nmero de procesadores del equipo reduce tambin las ventajas de la paralelizacin. Si se ejecutan varios subprocesos enlazados a clculos en un nico procesador, no se gana en velocidad. Por tanto, debe tener cuidado para no paralelizar en exceso un bucle. El escenario ms comn en el que se puede producir un exceso de paralelizacin son los bucles anidados. En la mayora de los casos, es mejor paralelizar nicamente el bucle exterior, a menos que se cumplan una o ms de las siguientes condiciones:
Se sabe que el bucle interno es muy largo. Se realiza un clculo caro en cada pedido. (La operacin que se muestra en el ejemplo no es cara.) Se sabe que el sistema de destino tiene suficientes procesadores como para controlar el nmero de subprocesos que se producirn al paralelizar la consulta de cust.Orders.
En todos los casos, la mejor manera de determinar la forma ptima de la consulta es mediante la prueba y la medicin. Evitar llamadas a mtodos que no son seguros para subprocesos La escritura en mtodos de instancia que no son seguros para subprocesos de un bucle en paralelo puede producir daos en los datos, que pueden pasar o no inadvertidos para el programa. Tambin puede dar lugar a excepciones. En el siguiente ejemplo, varios subprocesos estaran intentando llamar simultneamente al mtodo FileStream.WriteByte, lo que no se admite en la clase. FileStream fs = File.OpenWrite(path); byte[] bytes = new Byte[10000000]; // ... Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i])); Limitar las llamadas a mtodos seguros para subprocesos La mayora de los mtodos estticos de .NET Framework son seguros para subprocesos y se les pueden llamar simultneamente desde varios subprocesos. Sin embargo, incluso en estos casos, la sincronizacin que esto supone puede conducir a una ralentizacin importante en la consulta. Ser consciente de los problemas de afinidad de los subprocesos Algunas tecnologas, como la interoperabilidad COM para componentes STA (apartamento de un nico subproceso), Windows Forms y Windows Presentation Foundation (WPF), imponen restricciones de afinidad de subprocesos que exigen que el cdigo se ejecute en un subproceso determinado. Por ejemplo, tanto en Windows Forms como en WPF, solo se puede tener acceso a un control en el subproceso donde se cre. Por ejemplo, esto significa que no puede actualizar un control de lista desde un bucle paralelo a menos que configure el programador del subproceso para que programe trabajo solo en el subproceso de la interfaz de usuario. Para obtener ms informacin, vea Cmo: Programar el trabajo en un contexto de sincronizacin especificado. Tener precaucin cuando se espera en delegados a los que llama Parallel.Invoke En algunas circunstancias, Task Parallel Library incluir una tarea, lo que significa que se ejecuta en la tarea del subproceso que se est ejecutando actualmente. (Para obtener ms informacin, vea Programadores de tareas). Esta optimizacin de rendimiento puede acabar en interbloqueo en algunos casos. Por ejemplo, dos tareas podran ejecutar el mismo cdigo de delegado, que seala cundo se genera un evento, y despus esperar a que la otra tarea seale. Si la segunda tarea est alineada en el mismo subproceso que la primera y la primero entra en un estado de espera, la segunda tarea nunca podr sealar su evento. Para evitar que suceda, puede especificar un tiempo de espera en la operacin de espera o utilizar constructores de subproceso explcitos para ayudar a asegurarse de que una tarea no puede bloquear la otra. No se debe suponer que las iteraciones de Foreach, For y ForAll siempre se ejecutan en paralelo Es importante tener presente que las iteraciones individuales de un bucle For, ForEach o ForAll<TSource> tal vez no tengan que ejecutarse en paralelo. Por consiguiente, se debe evitar escribir cdigo cuya exactitud dependa de la ejecucin en paralelo de las iteraciones o de la ejecucin de las iteraciones en algn orden concreto. Por ejemplo, es probable que este cdigo lleve a un interbloqueo: ManualResetEventSlim mre = new ManualResetEventSlim(); Enumerable.Range(0, Environment.ProcessorCount * 100) .AsParallel() .ForAll((j) => { if (j == Environment.ProcessorCount) { Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j); mre.Set(); } else { Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j); mre.Wait();
} }); //deadlocks
En este ejemplo, una iteracin establece un evento y el resto de las iteraciones esperan el evento. Ninguna de las iteraciones que esperan puede completarse hasta que se haya completado la iteracin del valor de evento. Sin embargo, es posible que las iteraciones que esperan bloqueen todos los subprocesos que se utilizan para ejecutar el bucle paralelo, antes de que la iteracin del valor de evento haya tenido oportunidad de ejecutarse. Esto produce un interbloqueo: la iteracin del valor de evento nunca se ejecutar y las iteraciones que esperan nunca se activarn. En concreto, una iteracin de un bucle paralelo no debe esperar nunca otra iteracin del bucle para progresar. Si el bucle paralelo decide programar las iteraciones secuencialmente pero en el orden contrario, se producir un interbloqueo. Evitar la ejecucin de bucles en paralelo en el subproceso de la interfaz de usuario
Es importante mantener la interfaz de usuario de la aplicacin (UI) capaz de reaccionar. Si una operacin contiene bastante trabajo para garantizar la paralelizacin, no se debera ejecutar en el subproceso de la interfaz de usuario. Conviene descargarla para que se ejecute en un subproceso en segundo plano. Por ejemplo, si desea utilizar un bucle paralelo para calcular datos que despus se presentarn en un control de IU, considere ejecutar el bucle dentro de una instancia de la tarea, en lugar de directamente en un controlador de eventos de IU. Solo si el clculo bsico se ha completado se deberan calcular las referencias de la actualizacin de nuevo en el subproceso de la interfaz de usuario. Si ejecuta bucles paralelos en el subproceso de la interfaz de usuario, tenga el cuidado de evitar la actualizacin de los controles de la interfaz de usuario desde el interior del bucle. Si se intenta actualizar los controles de la interfaz de usuario desde dentro de un bucle paralelo que se est ejecutando en el subproceso de la interfaz de usuario, se puede llegar a daar el estado, a producir excepciones, actualizaciones atrasadas e incluso interbloqueos, dependiendo de cmo se invoque la actualizacin de la interfaz de usuario. En el siguiente ejemplo, el bucle paralelo bloquea el subproceso de la interfaz de usuario en el que se est ejecutando hasta que todas las iteraciones se completan. Sin embargo, si se est ejecutando una iteracin del bucle en un subproceso en segundo plano (como puede hacer For), la llamada a Invoke produce que se enve un mensaje al subproceso de la interfaz de usuario, que se bloquea mientras espera a que ese mensaje se procese. Puesto que se bloquea el subproceso de la interfaz de usuario cuando se ejecuta For, el mensaje no se procesa nunca y el subproceso de la interfaz de usuario se interbloquea. private void button1_Click(object sender, EventArgs e) { Parallel.For(0, N, i => { // do work for i button1.Invoke((Action)delegate { DisplayProgress(i); }); }); } En el siguiente ejemplo se muestra cmo evitar el interbloqueo mediante la ejecucin del bucle dentro de una instancia de la tarea. El bucle no bloquea el subproceso de la interfaz de usuario y se puede procesar el mensaje. private void button1_Click(object sender, EventArgs e) { Task.Factory.StartNew(() => Parallel.For(0, N, i => { // do work for i button1.Invoke((Action)delegate { DisplayProgress(i); }); }) ); }