Para ver el artículo en inglés, active la casilla Inglés. También puede ver el texto en inglés en una ventana emergente si pasa el puntero del mouse por el texto.
Traducción
Inglés
Se recomienda usar Visual Studio 2017
Esta documentación está archivada y no tiene mantenimiento.

Paralelismo de tareas (Task Parallel Library)

Como indica su nombre, la biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) se basa en el concepto de tarea ("task" en inglés). El término paralelismo de tareas hace referencia a la ejecución simultánea de una o varias tareas independientes. Una tarea representa una operación asincrónica y, en ciertos aspectos, se asemeja a la creación de un nuevo subproceso o elemento de trabajo ThreadPool, pero con un nivel de abstracción mayor. Las tareas proporcionan dos ventajas fundamentales:

  • Un uso más eficaz y más 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 número 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 número 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 programación 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, programación personalizada, y más.

Por estos dos motivos, en .NET Framework 4, las tareas son las API preferidas para escribir código paralelo, multiproceso y asincrónico.

El método Parallel.Invoke proporciona una manera conveniente de ejecutar cualquier número de instrucciones arbitrarias simultáneamente. Pase un delegado Action por cada elemento de trabajo. La manera más fácil de crear estos delegados es con expresiones lambda. La expresión lambda puede llamar a un método con nombre o proporcionar el código alineado. En el siguiente ejemplo se muestra una llamada a Invoke básica que crea e inicia dos tareas que se ejecutan a la vez.

NotaNota

En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL. Si no está familiarizado con las expresiones lambda en C# o Visual Basic, vea Expresiones lambda en PLINQ y TPL.


Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());


NotaNota

El número de instancias de Task que Invoke crea en segundo plano no es necesariamente igual al número de delegados que se proporcionan. La TPL puede emplear varias optimizaciones, sobre todo con grandes números de delegados.

Para obtener más información, vea Cómo: Usar Parallel.Invoke para ejecutar operaciones paralelas.

Para tener un mayor control de la ejecución de tareas o para devolver un valor de la tarea, debe trabajar con objetos Task más explícitamente.

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 métodos y propiedades a los que se puede obtener acceso desde el subproceso que realiza la llamada a lo largo de la duración 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 finalización, si se ha cancelado o si se ha producido una excepción. El estado se representa mediante la enumeración TaskStatus.

Cuando se crea una tarea, se proporciona un delegado de usuario que encapsula el código que la tarea va a ejecutar. El delegado se puede expresar como un delegado con nombre, un método anónimo o una expresión lambda. Las expresiones lambda pueden contener una llamada a un método con nombre, tal y como se muestra en el siguiente ejemplo.


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


            /* Output:
             * Hello from the joining thread.
             * Hello from taskA. 
             */



También se puede usar el método StartNew para crear e iniciar una tarea en una sola operación. Esta es la manera preferida de crear e iniciar tareas si la creación y la programación no tienen que ser independientes, como se muestra en el ejemplo siguiente


// 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 estática que devuelve una instancia predeterminada de TaskFactory, por lo que se puede llamar al método 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 pública que contiene el resultado del cálculo. Las tareas se ejecutan de forma asincrónica y pueden completarse en cualquier orden. Si se obtiene acceso a Result antes de que el cálculo 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;


Para obtener más información, vea Cómo: Devolver un valor de una tarea.

Cuando se usa una expresión lambda para crear el delegado de una tarea, se obtiene acceso a todas las variables que están visibles en ese momento en el código fuente. Sin embargo, en algunos casos, sobre todo en los bucles, una expresión lambda no captura la variable como cabría esperar. Captura solo el valor final, no el valor tal y como se transforma después de cada iteración. Puede obtener acceso al valor en cada iteración si proporciona un objeto de estado a una tarea a través de su constructor, como se muestra en el ejemplo siguiente:



       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. Además, el paso de los datos a través del constructor podría proporcionar una pequeña ventaja de rendimiento en algunos escenarios.

Cada tarea recibe un identificador entero que la identifica de manera inequívoca en un dominio de aplicación y al que se puede obtener acceso mediante la propiedad Id. El identificador resulta útil para ver información 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. Para obtener más información acerca de cómo se pueden ver los identificadores de tareas en el depurador, consulte Uso de la ventana Tareas paralelas.

La mayoría de las API que crean tareas proporcionan sobrecargas que aceptan un parámetro TaskCreationOptions. Al especificar una de estas opciones, se le está indicando al programador cómo se programa la tarea en el grupo de subprocesos. En la tabla siguiente se muestran las diversas opciones de creación de tareas.

Elemento

Descripción

None

Es la opción predeterminada si no se especifica ninguna opción. El programador usa su heurística predeterminada para programar la tarea.

PreferFairness

Especifica que la tarea debe programarse de modo que las tareas creadas anteriormente tengan más posibilidades de ejecutarse antes y que las tareas posteriormente tengan más posibilidades de ejecutarse después.

LongRunning

Especifica que la tarea representa una operación de ejecución prolongada.

AttachedToParent

Especifica que una tarea debe crearse como un elemento secundario asociado de la tarea actual, si existe. Para obtener más información, vea Tareas anidadas y tareas secundarias.

Las opciones pueden combinarse con una operación OR bit a bit. En el ejemplo siguiente se muestra una tarea que tiene las opciones LongRunning y PreferFairness.


var task3 = new Task(() => MyLongRunningMethod(),
                    TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();


El método Task.ContinueWith y el método Task<TResult>.ContinueWith permiten especificar que una tarea se inicie cuando la tarea anterior se complete. Al delegado de la tarea de continuación se le pasa una referencia de la tarea anterior para que pueda examinar su estado. Además, la tarea de continuación 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 continuación. En el ejemplo siguiente, el código del programa inicia getData; a continuación, se inicia analyzeData automáticamente 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 método 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 métodos ContinueWhenAll y ContinueWhenAny permiten continuar a partir de varias tareas. Para obtener más información, vea Tareas de continuación y Cómo: Encadenar varias tareas con continuaciones.

Cuando el código de usuario que se está ejecutando en una tarea crea una nueva tarea y no especifica la opción AttachedToParent, la nueva tarea no se sincroniza con la tarea externa de ninguna manera especial. Este tipo de tareas se denominan tareas anidadas desasociadas. En el siguiente ejemplo se muestra una tarea que crea una tarea anidada desasociada.


            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.

             */



Observe que la tarea externa no espera a que la tarea anidada se complete.

Cuando el código de usuario que se está ejecutando en una tarea crea una tarea con la opción AttachedToParent, la nueva tarea se concibe como una tarea secundaria de la tarea original, que se denomina tarea primaria. Puede usar la opción AttachedToParent para expresar el paralelismo de tareas estructurado, ya que la tarea primaria espera implícitamente a que todas las tareas secundarias se completen. En el siguiente ejemplo se muestra una tarea que crea una tarea secundaria:


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.
 */


Para obtener más información, vea Tareas anidadas y tareas secundarias.

El tipo System.Threading.Tasks.Task y el tipo System.Threading.Tasks.Task<TResult> proporcionan varias sobrecargas de un método Task.Wait y Task<TResult>.Wait que permiten esperar a que una tarea se complete. Además, las sobrecargas del método Task.WaitAll estático y del método 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.

En el siguiente ejemplo se muestra el modelo básico donde el control de excepciones no está implicado.


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

// Continue on this thread...


Para obtener un ejemplo que muestra el control de excepciones, vea Cómo: Controlar excepciones iniciadas por tareas.

Algunas sobrecargas permiten especificar un tiempo de espera, mientras que otras toman un objeto CancellationToken adicional como parámetro de entrada, de modo que la espera puede cancelarse mediante programación o en respuesta a los datos proporcionados por el usuario.

Cuando se espera a una tarea, se espera implícitamente a todos los elementos secundarios de esa tarea que se crearon con la opción AttachedToParent de TaskCreationOptions. Task.Wait devuelve un valor inmediatamente si la tarea ya se ha completado. Un método Wait producirá las tareas generadas por una tarea incluso si se llama a este método Wait una vez completada la tarea.

Para obtener más información, vea Cómo: Esperar a que una o varias tareas se completen.

Cuando una tarea produce una o varias excepciones, las excepciones se encapsulan en un objeto AggregateException. Esa excepción 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 código de llamada puede controlar las excepciones a través de los métodos Wait, WaitAll o WaitAny o de la propiedad Result() de la tarea o grupo de tareas, mientras incluye el método Wait en un bloque try-catch.

El subproceso de unión también 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 excepción no controlada desencadene el comportamiento de propagación de la excepción que anula el proceso cuando el objeto ha finalizado.

Para obtener más información sobre excepciones y tareas, vea Control de excepciones (Task Parallel Library) y Cómo: Controlar excepciones iniciadas por tareas.

La clase Task admite la cancelación cooperativa y su completa integración con las clases System.Threading.CancellationTokenSource y System.Threading.CancellationToken, que son nuevas en .NET Framework versión 4. Muchos de los constructores de la clase System.Threading.Tasks.Task toman un objeto CancellationToken como parámetro de entrada. Muchas de las sobrecargas de StartNew toman también CancellationToken.

Puede crear el token y emitir la solicitud de cancelación posteriormente usando la clase CancellationTokenSource. A continuación, debe pasar el token a Task como argumento y hacer referencia al mismo token también en el delegado de usuario, que se encarga de responder a una solicitud de cancelación. Para obtener más información, vea Cancelación de tareas y Cómo: Cancelar una tarea y sus elementos secundarios.

La clase TaskFactory proporciona métodos estáticos que encapsulan algunos modelos comunes de creación e inicio de tareas y tareas de continuación.

El objeto TaskFactory predeterminado es accesible como propiedad estática de la clase Task o de la clase Task<TResult>. También pueden crearse directamente instancias de TaskFactory y especificar varias opciones entre las que se incluyan las opciones CancellationToken, TaskCreationOptions, TaskContinuationOptions o TaskScheduler. Cualquier opción 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 enumeración TaskCreationOptions, en cuyo caso las opciones de la tarea reemplazarán a las del generador de tareas.

En algunos casos, es posible que desee usar un objeto Task para encapsular alguna operación asincrónica ejecutada por un componente externo en lugar de su propio usuario delegado. Si la operación se basa en el patrón Begin/End del modelo de programación asincrónica, puede usar los métodos FromAsync. Si no es este el caso, puede usar el objeto TaskCompletionSource<TResult> para encapsular la operación en una tarea y, de este modo, aprovechar algunas de las ventajas de programación de Task, como por ejemplo, su compatibilidad con la propagación de excepciones y el uso de continuaciones. Para obtener más información, vea TaskCompletionSource<TResult>.

La mayoría de los desarrolladores de aplicaciones o bibliotecas no prestan atención 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 ejecución en el equipo host sea lo más eficaz posible. Si necesita tener un control más minucioso sobre los detalles de programación, 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. Para obtener más información, vea TaskScheduler.

TPL tiene varios tipos públicos nuevos que resultan útiles tanto en escenarios en paralelo como en escenarios secuenciales. Entre ellos, se incluyen diversas clases de colecciones multiproceso rápidas y escalables del espacio de nombres System.Collections.Concurrent y varios tipos nuevos de sincronización, como SemaphoreLock y System.Threading.ManualResetEventSlim, que resultan más eficaces que sus predecesores en tipos concretos de cargas de trabajo. Otros tipos nuevos de .NET Framework versión 4, como System.Threading.Barrier y System.Threading.SpinLock, proporcionan una funcionalidad que no estaba disponible en versiones anteriores. Para obtener más información, vea Estructuras de datos para la programación paralela.

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>. También puede usar métodos de extensión para extender la funcionalidad de las clases Task y Task<TResult>. Para obtener más información sobre los métodos de extensión, vea Métodos de extensión (Guía de programación de C#) y Métodos de extensión (Visual Basic).

Si 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>. Además, no puede usar los mecanismos de continuación de tarea proporcionados por Task, Task<TResult>, TaskFactory y TaskFactory<TResult> para crear instancias del tipo de tarea personalizada porque también estos mecanismos crean solo objetos Task y Task<TResult>.

Título

Descripción

Tareas de continuación

Describe el funcionamiento de las continuaciones.

Tareas anidadas y tareas secundarias

Describe la diferencia entre las tareas secundarias y las tareas anidadas.

Cancelación de tareas

Describe la compatibilidad con la cancelación que está integrada en la clase Task.

Control de excepciones (Task Parallel Library)

Describe cómo se controlan excepciones en subprocesos simultáneos.

Cómo: Usar Parallel.Invoke para ejecutar operaciones paralelas

Describe cómo usar Invoke.

Cómo: Devolver un valor de una tarea

Describe cómo devolver valores de tareas.

Cómo: Esperar a que una o varias tareas se completen

Describe cómo esperar tareas.

Cómo: Cancelar una tarea y sus elementos secundarios

Describe cómo cancelar tareas.

Cómo: Controlar excepciones iniciadas por tareas

Describe cómo controlar las excepciones iniciadas por tareas.

Cómo: Encadenar varias tareas con continuaciones

Describe cómo ejecutar una tarea cuando se completa otra tarea.

Cómo: Recorrer un árbol binario con tareas paralelas

Describe cómo utilizar tareas para atravesar un árbol binario.

Paralelismo de datos (Task Parallel Library)

Describe cómo usar For y ForEach para crear bucles paralelos sobre los datos.

Programación paralela en .NET Framework

Nodo de nivel superior de la programación en paralelo de .NET.

date

Historial

Motivo

Marzo de 2011

Información adicional sobre cómo se hereda de las clases Task y Task<TResult>.

Mejora de la información.

Mostrar: