Está en la página 1de 107

Programación Paralela en .

NET Framework
MCT: Luis Dueñas Pag 1 de 107
Programación Paralela en .NET Framework
Muchos equipos y estaciones de trabajo tienen dos o cuatro núcleos (es decir, CPU) que permiten
ejecutar varios subprocesos simultáneamente. Se espera que los equipos en un futuro cercano
tengan significativamente más núcleos. Para aprovecharse del hardware de hoy y del mañana, puede
paralelizar el código para distribuir el trabajo entre varios procesadores. En el pasado, la
paralelización requería manipulación de bajo nivel de los subprocesos y bloqueos. Visual Studio 2010
y .NET Framework 4 mejoran la compatibilidad para la programación paralela proporcionando un
nuevo runtime, nuevos tipos de biblioteca de clases y nuevas herramientas de diagnóstico. Estas
características simplifican el desarrollo en paralelo, de modo que pueda escribir código paralelo
eficaz, específico y escalable de forma natural sin tener que trabajar directamente con subprocesos ni
el bloque de subprocesos. La siguiente ilustración proporciona una información general de alto nivel
de la arquitectura de programación paralela en .NET Framework 4.


1. Task Parallel Library
La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) es un
conjunto de API y tipos públicos de los espacios de nombres System.Threading.Tasks y
System.Threading de .NET Framework versión 4. El propósito de TPL es hacer los desarrolladores de
software más productivos simplificando el proceso de agregar paralelismo y simultaneidad a las
aplicaciones. TPL escala dinámicamente el grado de simultaneidad para utilizar todos los
procesadores que están disponibles eficazmente. Además, la TPL se encarga de la división del
trabajo, la programación de los subprocesos en ThreadPool, la compatibilidad con la cancelación, la
administración de los estados y otros detalles de bajo nivel. Al utilizar la TPL, el usuario puede
optimizar el rendimiento del código mientras se centra en el trabajo para el que el programa está
diseñado.
A partir de .NET Framework 4, la TPL es el modo preferido de escribir código paralelo y multiproceso.
Sin embargo, no todo el código se presta para la paralelización; por ejemplo, si un bucle realiza solo
una cantidad reducida de trabajo en cada iteración o no se ejecuta para un gran número de
iteraciones, la sobrecarga de la paralelización puede dar lugar a una ejecución más lenta del código.
Además, al igual que cualquier código multiproceso, la paralelización hace que la ejecución del
programa sea más compleja. Aunque la TPL simplifica los escenarios de multithreading,
recomendamos tener conocimientos básicos sobre conceptos de subprocesamiento, por ejemplo,
bloqueos, interbloqueos y condiciones de carrera, para usar la TPL eficazmente.
1.1. Paralelismo de datos (Task Parallel Library)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 2 de 107
El paralelismo de datos hace referencia a los escenarios en los que la misma operación se realiza
simultáneamente (es decir, en paralelo) en elementos de una colección o matriz de origen. Varias
sobrecargas de los métodos 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 colección de origen para que varios subprocesos puedan funcionar
simultáneamente en segmentos diferentes. TPL admite el paralelismo de datos a través de la clase
System.Threading.Tasks.Parallel. Esta clase proporciona las implementaciones paralelas basadas en
método de los bucles for y foreach (For y For Each en Visual Basic). Se escribe la lógica del bucle para
un bucle Parallel.For o Parallel.ForEach de forma muy similar a como se escribiría un bucle secuencial.
No tiene que crear los subprocesos ni poner en la cola los elementos de trabajo. En bucles básicos,
no es preciso tomar bloqueos. TPL administra todo el trabajo de bajo nivel. En el siguiente ejemplo
de código se muestra un bucle foreach simple y su equivalente paralelo.
' Sequential version
For Each item In sourceCollection
Process(item)
Next
' Parallel equivalent
Parallel.ForEach(sourceCollection, Sub(item) Process(item))
Cuando un bucle paralelo se ejecuta, la TPL crea particiones del origen de datos para que el bucle
pueda funcionar simultáneamente en varias partes. En segundo plano, el programador de tareas crea
particiones de la tarea según 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 métodos Parallel.ForEach y Parallel.For tienen varias sobrecargas que permiten detener o ejecutar
la ejecución 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 aplicación auxiliar que habilitan esta funcionalidad son ParallelLoopState,
ParallelOptions y ParallelLoopResult, CancellationToken y CancellationTokenSource.

1.1.1. Cómo: Escribir un bucle Parallel.For simple
En este ejemplo se muestra cómo utilizar la sobrecarga más simple del método Parallel.For para
calcular el producto de dos matrices. También se muestra cómo utilizar la clase
System.Diagnostics.Stopwatch para comparar el rendimiento de un bucle paralelo con un bucle no
paralelo.
Ejemplo
' How to: Write a Simple Parallel.For Loop
Imports System.Threading.Tasks
Module MultiplyMatrices
#Region "Sequential_Loop"
Sub MultiplyMatricesSequential(ByVal matA As Double(,), ByVal matB As Double(,), ByVal result As Double(,))
Dim matACols As Integer = matA.GetLength(1)
Dim matBCols As Integer = matB.GetLength(1)
Dim matARows As Integer = matA.GetLength(0)
For i As Integer = 0 To matARows - 1
For j As Integer = 0 To matBCols - 1
For k As Integer = 0 To matACols - 1
result(i, j) += matA(i, k) * matB(k, j)
Next
Next
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 3 de 107
Next
End Sub
#End Region
#Region "Parallel_Loop"
Private Sub MultiplyMatricesParallel(ByVal matA As Double(,), ByVal matB As Double(,), ByVal result As Double(,))
Dim matACols As Integer = matA.GetLength(1)
Dim matBCols As Integer = matB.GetLength(1)
Dim matARows As Integer = matA.GetLength(0)
' A basic matrix multiplication. Parallelize the outer loop to partition the source array by rows.
Parallel.For(0, matARows, Sub(i)
For j As Integer = 0 To matBCols - 1
' Use a temporary to improve parallel performance.
Dim temp As Double = 0
For k As Integer = 0 To matACols - 1
temp += matA(i, k) * matB(k, j)
Next
result(i, j) += temp
Next
End Sub)
End Sub
#End Region
#Region "Main"
Sub Main(ByVal args As String())
' 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.
Dim colCount As Integer = 180
Dim rowCount As Integer = 2000
Dim colCount2 As Integer = 270
Dim m1 As Double(,) = InitializeMatrix(rowCount, colCount)
Dim m2 As Double(,) = InitializeMatrix(colCount, colCount2)
Dim result As Double(,) = New Double(rowCount - 1, colCount2 - 1) {}
' First do the sequential version.
Console.WriteLine("Executing sequential loop...")
Dim stopwatch As 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 - 1, colCount2 - 1) {}
' 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)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 4 de 107
' Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
#End Region
#Region "Helper_Methods"
Function InitializeMatrix(ByVal rows As Integer, ByVal cols As Integer) As Double(,)
Dim matrix As Double(,) = New Double(rows - 1, cols - 1) {}
Dim r As New Random()
For i As Integer = 0 To rows - 1
For j As Integer = 0 To cols - 1
matrix(i, j) = r.[Next](100)
Next
Next
Return matrix
End Function
Sub OfferToPrint(ByVal rowCount As Integer, ByVal colCount As Integer, ByVal matrix As Double(,))
Console.WriteLine("Computation complete. Print results? y/n")
Dim c As Char = Console.ReadKey().KeyChar
If c = "y"c OrElse c = "Y"c Then
Console.WindowWidth = 168
Console.WriteLine()
For x As Integer = 0 To rowCount - 1
Console.WriteLine("ROW {0}: ", x)
For y As Integer = 0 To colCount - 1
Console.Write("{0:#.##} ", matrix(x, y))
Next
Console.WriteLine()
Next
End If
End Sub
#End Region
End Module
Puede utilizar la sobrecarga más básica del método For si no necesita cancelar ni interrumpir las
iteraciones, ni mantener un estado local de subproceso.
Al paralelizar un código, 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 combinación de una cantidad pequeña de trabajo y los efectos no deseados en la memoria caché
puede producir la degradación 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
mayoría de los sistemas.
Delegado
El tercer parámetro 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 parámetros
como si tiene uno o dieciséis. En Visual Basic, el comportamiento de Action se define con Sub. En el
ejemplo se utiliza una expresión lambda para crear el delegado, pero también se puede crear de
otras formas.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 5 de 107
Valor de iteración
El delegado toma un único parámetro de entrada cuyo valor es la iteración actual. El runtime
proporciona este valor de iteración y su valor inicial es el índice del primer elemento del segmento
(partición) del origen que se procesa en el subproceso actual.
Si requiere más control sobre el nivel de simultaneidad, utilice una de las sobrecargas que toma un
parámetro de entrada System.Threading.Tasks.ParallelOptions, como: Parallel.For(Int32, Int32,
ParallelOptions, Action(Of 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 iteración del bucle
de forma manual, ya que ParallelLoopResult almacena información como la última iteración que se
ejecutó hasta finalizar. Si se producen una o más excepciones en uno de los subprocesos, se inicia
System.AggregateException.
En el código de este ejemplo, no se usa el valor devuelto de For.
Análisis y rendimiento
Puede utilizar el Asistente de rendimiento para ver el uso de la CPU en el equipo. Como experimento,
aumente el número 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 cálculo. Si la matriz es
pequeña, la versión en serie se ejecutará más rápidamente debido a la sobrecarga de la
configuración del bucle paralelo.
Las llamadas sincrónicas a los recursos compartidos, como la consola o el sistema de archivos,
degradarán de forma significativa el rendimiento de un bucle paralelo. Al medir el rendimiento,
intente evitar llamadas como Console.WriteLine dentro del bucle.

1.1.2. Cómo: Escribir un bucle Parallel.ForEach simple
En este ejemplo, se muestra cómo se usa un bucle Parallel.ForEach para habilitar el paralelismo de
datos en cualquier origen de datos System.Collections.IEnumerable o
System.Collections.Generic.IEnumerable(Of T).
Ejemplo
' How to: Write a Simple Parallel.ForEach Loop
Imports System.Threading
Imports System.Threading.Tasks
Imports System.Drawing
Module ForEachDemo
Sub Main()
' A simple source for demonstration purposes. Modify this path as necessary.
Dim files As String() = System.IO.Directory.GetFiles("C:\Users\Public\Pictures\Sample Pictures", "*.jpg")
Dim newDir As String = "C:\Users\Public\Pictures\Sample Pictures\Modified"
System.IO.Directory.CreateDirectory(newDir)
' Method signature: Parallel.ForEach(IEnumerable<TSource> source, Action<TSource> body)
' Be sure to add a reference to System.Drawing.dll.
Parallel.ForEach(files, Sub(currentFile)
' The more computational work you do here, the greater
' the speedup compared to a sequential foreach loop.
Dim filename As String = System.IO.Path.GetFileName(currentFile)
Dim bitmap As New System.Drawing.Bitmap(currentFile)
bitmap.RotateFlip(System.Drawing.RotateFlipType.Rotate180FlipNone)
bitmap.Save(System.IO.Path.Combine(newDir, filename))
' Peek behind the scenes to see how work is parallelized.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 6 de 107
' But be aware: Thread contention for the Console slows down parallel loops!!!
Console.WriteLine("Processing {0} on thread {1}", filename,
Thread.CurrentThread.ManagedThreadId)
'close lambda expression and method invocation
End Sub)
' Keep the console window open in debug mode.
Console.WriteLine("Processing complete. Press any key to exit.")
Console.ReadKey()
End Sub
End Module
Un ForEach trabajos del bucle como un bucle For. Se crea partición la recopilación del origen y el
trabajo se programa en varios subprocesos basados en el entorno del sistema. Cuantos más
procesadores tenga el sistema, más rápido se ejecutará el método paralelo. En algunas colecciones
de origen, puede resultar más rápido un bucle secuencial, en función del tamaño del origen y del tipo
de trabajo que se realice.
Para utilizar ForEach con una recopilación no genérica, puede utilizar el método de extensión Cast(Of
TResult) para convertir la recopilación en una recopilación genérica, como se muestra en el siguiente
ejemplo:
Parallel.ForEach(nonGenericCollection.Cast(Of Object), _
Sub(currentElement)
' ... work with currentElement
End Sub)
También puede utilizar LINQ Paralelo (PLINQ) para paralelizar procesamiento de origenes de datos
IEnumerable(Of T). PLINQ permite usar una sintaxis de consulta declarativa para expresar el
comportamiento del bucle.

1.1.3. Cómo: Detener o interrumpir un bucle Parallel.For
En el siguiente ejemplo se muestra cómo interrumpir un bucle For (o Salir de él en Visual Basic) y
también cómo detener un bucle. En este contexto, "interrumpir" significa completar todas las
iteraciones en todos los subprocesos que son anteriores a la iteración actual en el subproceso actual
y, a continuación, salir del bucle. " Pausa" significa detener todas las iteraciones en cuanto
conveniente.
Ejemplo
En este ejemplo se muestra un bucle For; sin embargo, se puede detener o interrumpir desde un
bucle ForEach de la misma manera. En un bucle ForEach, se genera un índice de iteración
internamente para cada uno de los elementos de cada partición.
' How to: Stop or Break from a Parallel.For Loop
Imports System.Collections.Concurrent
Imports System.Threading
Imports System.Threading.Tasks
Module ParallelForStop
Sub Main()
StopLoop()
BreakAtThreshold()
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Sub StopLoop()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 7 de 107
Console.WriteLine("Stop loop...")
Dim source As Double() = MakeDemoSource(1000, 1)
Dim results As New ConcurrentStack(Of Double)()
' i is the iteration variable. loopState is a compiler-generated ParallelLoopState
Parallel.For(0, source.Length, Sub(i, loopState)
' Take the first 100 values that are retrieved from anywhere in the source.
If i < 100 Then
' Accessing shared object on each iteration is not efficient. See remarks.
Dim d As Double = Compute(source(i))
results.Push(d)
Else
loopState.[Stop]()
Exit Sub
End If
' Close lambda expression.
End Sub)
' Close Parallel.For
Console.WriteLine("Results contains {0} elements", results.Count())
End Sub
Sub BreakAtThreshold()
Dim source As Double() = MakeDemoSource(10000, 1.0002)
Dim results As New ConcurrentStack(Of Double)()
' Store all values below a specified threshold.
Parallel.For(0, source.Length, Function(i, loopState)
Dim d As Double = Compute(source(i))
results.Push(d)
If d > 0.2 Then
' Might be called more than once!
loopState.Break()
Console.WriteLine("Break called at iteration {0}. d = {1} ", i, d)
Thread.Sleep(1000)
End If
Return d
End Function)
Console.WriteLine("results contains {0} elements", results.Count())
End Sub
Function Compute(ByVal d As Double) As Double
'Make the processor work just a little bit.
Return Math.Sqrt(d)
End Function
' Create a contrived array of monotonically increasing' values for demonstration purposes.
Function MakeDemoSource(ByVal size As Integer, ByVal valToFind As Double) As Double()
Dim result As Double() = New Double(size - 1) {}
Dim initialval As Double = 0.01
For i As Integer = 0 To size - 1
initialval *= valToFind
result(i) = initialval
Next
Return result
End Function
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 8 de 107
End Module
En un bucle ParallelFor u [Overload:System.Threading.Tasks.Parallel.Parallel.ForEach`1], no se puede
usar la misma instrucción break o Exit que se utiliza en un bucle secuencial porque estas
construcciones de lenguaje son válidas para los bucles, y un "bucle" paralelo es realmente un
método, no un bucle. En su lugar, se usan los métodos Break o Stop. Algunas de las sobrecargas de
Parallel.For aceptan Action<int, ParallelLoopState> (Action(Of Integer, ParallelLoopState) en Visual
Basic) como parámetro de entrada. El runtime crea en segundo plano el objeto ParallelLoopState, al
que puede dar cualquier nombre que desee en la expresión lambda.
En el siguiente ejemplo, el método requiere sólo 100 valores de la secuencia de origen y no importa
qué elementos se recuperan. En este caso se usa el método Stop, porque indica todas las iteraciones
del bucle (incluidas las que comenzaron antes de la iteración actual en otros subprocesos), para
detenerse en cuanto sea conveniente.
En el segundo método 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 todavía no se hayan procesado los elementos anteriores en el origen. La interrupción
hará que otros subprocesos abandonen el trabajo en segmentos posteriores (si están ocupados en
alguno) y que completen el procesamiento de todos los elementos anteriores antes de salir del bucle.
Es importante entender que después de llamar a Stop o Break, otros subprocesos en un bucl e
pueden seguir ejecutándose durante algún tiempo, pero esto no está bajo el control del
desarrollador de la aplicación. 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 más datos en la colección.

1.1.4. Cómo: Escribir un bucle Parallel.For que tenga variables locales de
subproceso
En este ejemplo se muestra cómo utilizar variables locales de subproceso para almacenar y recuperar
el estado de cada tarea independiente que se crea en un bucle For. Si se usan datos locales de
subproceso, se puede evitar la sobrecarga de sincronizar un número grande de accesos al estado
compartido. En lugar de escribir en un recurso compartido en cada iteración, calcula y almacena el
valor hasta que se completan todas las iteraciones de la tarea. A continuación, puede escribir el
resultado final una vez en el recurso compartido o pasarlo a otro método.
Ejemplo
'How to: Write a Parallel.For Loop That Has Thread-Local Variables
Imports System.Threading
Imports System.Threading.Tasks
Module ForWithThreadLocal
Sub Main()
Dim nums As Integer() = Enumerable.Range(0, 1000000).ToArray()
Dim total As Long = 0
' Use type parameter to make subtotal a Long type. Function will overflow otherwise.
Parallel.For(Of Long)(0, nums.Length, Function() 0, Function(j, [loop], subtotal)
subtotal += nums(j)
Return subtotal
End Function, Function(x) Interlocked.Add(total, x))
Console.WriteLine("The total is {0}", total)
Console.WriteLine("Press any key to exit")
Console.ReadKey()
End Sub
End Module
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 9 de 107
Los dos primeros parámetros de cada método For especifican los valores de iteración inicial y final.
En esta sobrecarga del método, el tercer parámetro es donde inicializa el estado local. " Estado local"
en este contexto significa una variable cuya duración se extiende desde inmediatamente antes de la
primera iteración del bucle en el subproceso actual hasta inmediatamente después de la última
iteración.
El tipo del tercer parámetro es Func(Of TResult) donde TResult es el tipo de la variable que
almacenará el estado subproceso local. Observe que en este ejemplo, una versión genérica del
método se utiliza, el parámetro de and type es largo (Long en Visual Basic) El parámetro de tipo
indica el tipo de la variable temporal que se utilizará para almacenar el estado subproceso local al
compilador. La expresión () => 0 (Function() 0 en Visual Basic) en este ejemplo significa que la
variable subproceso local se inicializa para poner a cero. Si el parámetro de tipo es un tipo de
referencia o un tipo de valor definido por el usuario, este ejemplo de Func se parecería al siguiente:
Function() new MyClass()
El cuarto parámetro de tipo es donde define la lógica del bucle. Las presentaciones de IntelliSense
que tiene un tipo de Func<int, ParallelLoopState, long, long> o Func(Of Integer, ParallelLoopState,
Long, Long). La expresión lambda espera tres parámetros de entrada en este mismo orden que
corresponde a estos tipos. El último parámetro de tipo es el tipo devuelto. En este caso, el tipo es
long porque es lo que se especificó en el parámetro de tipo For. Llamamos a esa variable subtotal en
la expresión lambda y la devolvemos. El valor devuelto se utiliza para inicializar el subtotal en cada
iteración subsiguiente. También puede considerar este último parámetro simplemente como un valor
que se pasa a cada iteración y después al delegado localFinally cuando se completa la última
iteración.
El quinto parámetro es donde define el método que se denominará un tiempo, cuando todas las
iteraciones en este subproceso hayan completado. El tipo del parámetro de entrada corresponde de
nuevo al parámetro de tipo del método For y al tipo que devuelve la expresión 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 iteración de cada subproceso.

1.1.5. Cómo: Escribir un bucle Parallel.ForEach que tenga variables
locales de subproceso
En el siguiente ejemplo se muestra cómo escribir un método ForEach que utiliza variables locales de
subproceso. Cuando un bucle ForEach se ejecuta, divide su colección de origen en varias particiones.
Cada partición obtendrá su propia copia de la variable "local de subproceso". (El término "local de
subproceso" es ligeramente inexacto, porque en algunos casos dos particiones se pueden ejecutar en
el mismo subproceso).
El código y los parámetros de este ejemplo se parecen mucho al método For correspondiente.
Ejemplo
Para utilizar una variable local de subproceso en un bucle ForEach, debe utilizar la versión del
método que toma dos parámetros type. El primer parámetro especifica el tipo del elemento de
origen y el segundo parámetro especifica el tipo de la variable local de subproceso.
El primer parámetro de entrada es el origen de datos y el segundo es la función que inicializará la
variable local de subproceso. El tercer parámetro de entrada es un Func(Of T1, T2, T3, TResult) que
invoca el bucle paralelo en cada iteración. Se proporciona el código para el delegado y el bucle pasa
los parámetros de entrada. Los parámetros 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 continuación, el método pasa a la iteración siguiente de
esta partición. Esta variable es distinta en todas las particiones del bucle.
El último parámetro de entrada del método ForEach es el delegado Action(Of T) que el método
invocará cuando todos los bucles se hayan completado. El método proporciona el valor final de la
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 10 de 107
variable local de subproceso para este subproceso (o partición del bucle) y proporciona el código
que captura el valor final y realiza cualquier acción necesaria para combinar el resultado de esta
partición con los resultados de las otras particiones. Como el tipo de delegado es Action(Of T), no
hay valor devuelto.
' How to: Write a Parallel.ForEach Loop That Has Thread-Local Variables
Imports System.Threading
Imports System.Threading.Tasks
Module ForEachThreadLocal
Sub Main()
Dim nums() As Integer = Enumerable.Range(0, 1000000).ToArray()
Dim total As Long = 0
' First type paramemter is the type of the source elements
' Second type parameter is the type of the local data (subtotal)
Parallel.ForEach(Of Integer, Long)(nums, Function() 0,
Function(elem, loopState, subtotal)
subtotal += nums(elem)
Return subtotal
End Function,
Sub(finalResult)
Interlocked.Add(total, finalResult)
End Sub)
Console.WriteLine("The result of Parallel.ForEach is {0}", total)
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
End Module

1.1.6. Cómo: Cancelar un bucle Parallel.For o ForEach
Los métodos Parallel.ForEach y Parallel.For admiten la cancelación a través del uso de tokens de
cancelación. En un bucle paralelo, se proporciona CancellationToken al método en el parámetro
ParallelOptions y después se agrega la llamada paralela en un bloque try-catch.
Ejemplo
En el ejemplo siguiente se muestra cómo cancelar una llamada a Parallel.ForEach. Puede aplicar el
mismo enfoque a una llamada Parallel.For.
' How to: Cancel a Parallel.For or ForEach Loop
Imports System.Threading
Imports System.Threading.Tasks
Module CancelParallelLoops
Sub Main()
Dim nums() As Integer = Enumerable.Range(0, 10000000).ToArray()
Dim cts As New CancellationTokenSource
' Use ParallelOptions instance to store the CancellationToken
Dim po As 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.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 11 de 107
Dim t As Task = Task.Factory.StartNew(Sub()
If Console.ReadKey().KeyChar = "c"c Then
cts.Cancel()
End If
Console.WriteLine(vbCrLf & "Press any key to exit.")
End Sub)
Try
' The error "Exception is unhandled by user code" will appear if "Just My Code"
' is enabled. This error is benign. You can press F5 to continue, or disable Just My Code.
Parallel.ForEach(nums, po, Sub(num)
Dim d As Double = Math.Sqrt(num)
Console.CursorLeft = 0
Console.Write("{0:##.##} on {1}", d, Thread.CurrentThread.ManagedThreadId)
po.CancellationToken.ThrowIfCancellationRequested()
End Sub)
Catch e As OperationCanceledException
Console.WriteLine(e.Message)
End Try
Console.ReadKey()
End Sub
End Module
Si el token que señala la cancelación es el mismo que se especifica en la instancia de ParallelOptions,
el bucle paralelo producirá una OperationCanceledException única en la cancelación. Si algún otro
token produce la cancelación, el bucle producirá una AggregateException con
OperationCanceledException como InnerException.

1.1.7. Cómo: Controlar excepciones en bucles paralelos
El ParallelFory las sobrecargas ForEach no tienen ningún mecanismo especial para administrar
excepciones que se podrían producir. A este respecto, se asemejan a bucles for y foreach normales
(For y For Each en Visual Basic).
Cuando agregue su propia lógica 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 excepción iniciada en un subproceso puede hacer que se inicie otra excepción 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.
Nota
Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se interrumpe en
la línea que produce la excepción y muestra el mensaje de error "Excepción no controlada por el
código de usuario". Este error es benigno. Puede presionar F5 para continuar y ver el
comportamiento de control de excepciones que se muestra en el ejemplo siguiente. Para evitar que
Visual Studio se interrumpa con el primer error, desactive la casilla "Solo mi código" bajo
Herramientas, Opciones, Depuración, General.
Ejemplo
En este ejemplo, todas las excepciones se detectan y, a continuación, se encapsulan en la excepción
System.AggregateException que se produce. El llamador puede decidir qué excepciones se deben
administrar.
' How to: Handle Exceptions in Parallel Loops
Imports System.Collections.Concurrent
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 12 de 107
Imports System.Threading.Tasks
Module ExceptionsInLoops
Sub Main()
' Create some random data to process in parallel.
' There is a good probability this data will cause some exceptions to be thrown.
Dim data(1000) As Byte
Dim r As New Random()
r.NextBytes(data)
Try
ProcessDataInParallel(data)
Catch ae As AggregateException
' This is where you can choose which exceptions to handle.
For Each ex As Exception In ae.InnerExceptions
If (TypeOf (ex) Is ArgumentException) Then
Console.WriteLine(ex.Message)
Else
Throw ex
End If
Next
End Try
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Sub ProcessDataInParallel(ByVal data As Byte())
' Use ConcurrentQueue to enable safe enqueueing from multiple threads.
Dim exceptions As New ConcurrentQueue(Of Exception)
' Execute the complete loop and capture all exceptions.
Parallel.ForEach(Of Byte)(data, Sub(d)
Try
' Cause a few exceptions, but not too many.
If d < &H3 Then
Throw New ArgumentException(String.Format("value is {0:x}. Element must be greater than
&H3", d))
Else
Console.Write(d & " ")
End If
Catch ex As Exception
' Store the exception and continue with the loop.
exceptions.Enqueue(ex)
End Try
End Sub)
' Throw the exceptions here after the loop completes.
If exceptions.Count > 0 Then
Throw New AggregateException(exceptions)
End If
End Sub
End Module

1.1.8 Cómo: Acelerar cuerpos de bucle pequeños
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 13 de 107
Cuando un bucle For tiene un cuerpo pequeño, puede registrar un rendimiento más lento que el del
bucle secuencial equivalente. Este rendimiento más lento es consecuencia de la sobrecarga en la
participación de los datos y el costo de invocar un delegado en cada iteración del bucle. Para hacer
frente a estos escenarios, la clase Partitioner proporciona el método Create, que permite
proporcionar un bucle secuencial para el cuerpo de delegado de modo que el delegado solo se
invoque una vez por partición, en lugar de una vez por iteración.
Ejemplo
Imports System.Threading.Tasks
Imports System.Collections.Concurrent
Module PartitionDemo
Sub Main()
' Source must be array or IList.
Dim source = Enumerable.Range(0, 100000).ToArray()
' Partition the entire source array.
' Let the partitioner size the ranges.
Dim rangePartitioner = Partitioner.Create(0, source.Length)
Dim results(source.Length - 1) As Double
' Loop over the partitions in parallel. The Sub is invoked once per partition.
Parallel.ForEach(rangePartitioner, Sub(range, loopState)
' Loop over each range element without a delegate invocation.
For i As Integer = range.Item1 To range.Item2 - 1
results(i) = source(i) * Math.PI
Next
End Sub)
Console.WriteLine("Operation complete. Print results? y/n")
Dim input As Char = Console.ReadKey().KeyChar
If input = "y"c Or input = "Y"c Then
For Each d As Double In results
Console.Write("{0} ", d)
Next
End If
End Sub
End Module
El enfoque mostrado en este ejemplo es útil cuando el bucle realiza una cantidad de trabajo mínimo.
Cuando el trabajo se vuelve computationally caro, probablemente obtendrá el rendimiento mismo o
mejor utilizando For o bucle ForEach con el partitioner predeterminado.

1.1.9. Cómo: Recorrer en iteración directorios con la clase paralela
En muchos casos, la iteración del archivo es una operación que se puede paralelizar con facilidad.
Cómo: Recorrer en iteración directorios con PLINQ del tema muestra la manera más fácil de realizar
esta tarea para muchos escenarios. Sin embargo, las complicaciones pueden surgir cuando su código
tiene que repartir con el muchos tipos de excepciones que pueden surgir al tener acceso al sistema
de archivos. En el siguiente ejemplo se muestra un enfoque al problema. Utiliza una iteración basada
en pila para atravesar todos los archivos y carpetas bajo un directorio especificado y habilita su
código para detectar y administrar varias excepciones. Claro, la manera que administra las
excepciones depende de usted.
Ejemplo
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 14 de 107
En el siguiente ejemplo la iteración sobre los directorios se realiza secuencialmente, pero el
procesamiento de los archivos se hace en paralelo. Éste probablemente es el enfoque mejor al tener
una proporción archivo a directorio grande. También es posible paralelizar la iteración del directorio
y tiene acceso secuencialmente a cada archivo. Probablemente no es eficaz para paralelizar ambos
bucles a menos que esté destinando específicamente un equipo con un número grande de
procesadores. Sin embargo, como en todos los casos, debería probar completamente su aplicación
para determinar el enfoque mejor.
Imports System
Imports System.Collections.Generic
Imports System.Diagnostics
Imports System.IO
Imports System.Linq
Imports System.Text
Imports System.Threading
Imports System.Threading.Tasks
Module Parallel_File
Sub Main(ByVal args() As String)
TraverseTreeParallelForEach("C:\Program Files", Sub(f)
' For this demo we don't do anything with the data
' except to read it.
Dim data() As Byte = File.ReadAllBytes(f)
' For user interest, although it slows down the operation.
Console.WriteLine(f)
End Sub)
' Keep the console window open.
Console.ReadKey()
End Sub
Public Sub TraverseTreeParallelForEach(ByVal root As String, ByVal action As Action(Of String))
'Count of files traversed and timer for diagnostic output
Dim fileCount As Integer = 0
Dim sw As Stopwatch = Stopwatch.StartNew()
' Use this value to determine whether to parallelize
' file processing on each folder.
Dim procCount As Integer = System.Environment.ProcessorCount
' Data structure to hold names of subfolders to be
' examined for files.
Dim dirs As Stack(Of String) = New Stack(Of String)
If System.IO.Directory.Exists(root) = False Then
Throw New ArgumentException()
End If
dirs.Push(root)
While (dirs.Count > 0)
Dim currentDir As String = dirs.Pop()
Dim subDirs() As String = Nothing
Dim files() As String = Nothing
Try
subDirs = System.IO.Directory.GetDirectories(currentDir)
' An UnauthorizedAccessException exception will be thrown if we do not have discovery permission on a
folder or
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 15 de 107
' 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 e As UnauthorizedAccessException
Console.WriteLine(e.Message)
Continue While
Catch e As System.IO.DirectoryNotFoundException
Console.WriteLine(e.Message)
Continue While
End Try
Try
files = System.IO.Directory.GetFiles(currentDir)
Catch e As UnauthorizedAccessException
Console.WriteLine(e.Message)
Continue While
Catch e As System.IO.DirectoryNotFoundException
Console.WriteLine(e.Message)
Continue While
End Try
' 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 Then
For Each file In files
action(file)
fileCount = fileCount + 1
Next
Else
Parallel.ForEach(files, Function() 0, Function(file, loopState, localCount)
action(file)
localCount = localCount + 1
Return CType(localCount, Integer)
End Function,
Sub(c)
Interlocked.Exchange(fileCount, fileCount + c)
End Sub)
End If
Catch ae As AggregateException
ae.Handle(Function(ex)
If TypeOf (ex) Is UnauthorizedAccessException Then
' Here we just output a message and go on.
Console.WriteLine(ex.Message)
Return True
End If
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 16 de 107
' Handle other exceptions here if necessary...
Return False
End Function)
End Try
' Push the subdirectories onto the stack for traversal. This could also be done before handing the files.
For Each str As String In subDirs
dirs.Push(str)
Next
' For diagnostic purposes.
Console.WriteLine("Processed {0} files in {1} milleseconds", fileCount, sw.ElapsedMilliseconds)
End While
End Sub
End Module
En este ejemplo, el archivo que sincrónicamente se realiza E/S. Al tratar con archivos grandes o
conexiones de red lentas, podría ser preferible para tener acceso de forma asincrónica a los archivos.
Puede combinar las técnicas E/S asincrónico con iteración paralela.
Tenga en cuenta que si una excepción se produce en el subproceso principal, los subprocesos que se
son iniciados por el método ForEach podrían continuar ejecutándose. Para detener estos
subprocesos, puede establecer una variable Boolean en sus controladores de excepciones y
comprueba su valor en cada iteración del bucle paralelo. Si el valor indica que se ha producido una
excepción, utilice la variable ParallelLoopState para detenerse o interrumpir del bucle.

1.2. 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 paralelismo de tarea de
término hace referencia a una o más tareas independientes que se ejecutan concurrentemente. 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.
Crear y ejecutar tareas implícitamente
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 17 de 107
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.
Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())
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.
Crear y ejecutar tareas 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(Of 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.
Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))
' Start the task.
taskA.Start()
' Output a message from the joining thread.
Console.WriteLine("Hello from the joining 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. Ésta
es la manera preferida para crear e iniciar las tareas si la creación y programar no tiene que ser
separadose, como se muestra en el siguiente ejemplo
' Better: Create and start the task in one operation.
Dim taskA = Task.Factory.StartNew(Sub() Console.WriteLine("Hello from taskA."))
' Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.")
Tarea expone una propiedad Factory estática que devuelve una instancia predeterminada de
TaskFactory, para que pueda llamar al método como Task.Factory.StartNew(…). Asimismo, en este
ejemplo, dado que las tareas son de tipo System.Threading.Tasks.Task(Of 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 tiene acceso Result antes de que el
cálculo complete, la propiedad bloqueará el subproceso hasta que el valor esté disponible.
Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation1()),
Task(Of Double).Factory.StartNew(Function() DoComputation2()),
Task(Of Double).Factory.StartNew(Function() DoComputation3())}
Dim results() As Double
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 18 de 107
ReDim results(taskArray.Length)
For i As Integer = 0 To taskArray.Length
results(i) = taskArray(i).Result
Next
Al utilizar una expresión lambda para crear el delegado de una tarea, tiene el acceso a todas las
variables que están visibles en ese punto en su código fuente. Sin embargo, en algunos casos, un
lambda no captura notablemente dentro de los bucles, la variable como usted podría esperar. Sólo
captura el valor final, no el valor cuando deforma después de cada iteración. Puede tener acceso al
valor en cada iteración proporcionando un objeto de estados a una tarea a través de su constructor,
como se muestra en el siguiente ejemplo:
Class MyCustomData
Public CreationTime As Long
Public Name As Integer
Public ThreadNum As Integer
End Class
Sub 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 lamda,
' and compare results.
Dim taskArray() As Task
ReDim taskArray(10)
For i As Integer = 0 To taskArray.Length - 1
taskArray(i) = New Task(Sub(obj As Object)
Dim mydata = CType(obj, MyCustomData)
mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId
Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
mydata.Name, mydata.CreationTime, mydata.ThreadNum)
End Sub,
New MyCustomData With {.Name = i, .CreationTime = DateTime.Now.Ticks} )
taskArray(i).Start()
Next
End Sub
Se pasa por este estado como un argumento al delegado de tarea y es accesible del objeto de tarea
utilizando la propiedad AsyncState. Al pasar los datos a través del constructor, también, se podría
proporcionar una ventaja de rendimiento pequeña en algunos escenarios.
Identificador de tarea
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.
Opciones de creación de tareas
La mayoría de las API que crean las 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.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 19 de 107
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.
Las opciones pueden combinarse con una operación OR bit a bit. En el siguiente ejemplo se muestra
una tarea que tiene LongRunning y opción PreferFairness.
Dim task3 = New Task(Sub() MyLongRunningMethod(),
TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()
Crear continuaciones de tareas
El método Task.ContinueWith y el método Task(Of 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.
Dim getData As Task(Of Byte()) = New Task(Of Byte())(Function() GetFileData())
Dim analyzeData As Task(Of Double()) = getData.ContinueWith(Function(x) Analyze(x.Result))
Dim reportData As Task(Of String) = analyzeData.ContinueWith(Function(y As Task(Of Double))
Summarize(y.Result))
getData.Start()
System.IO.File.WriteAllText("C:\reportFolder\report.txt", reportData.Result)
Los métodos ContinueWhenAny y ContinueWhenAll le permiten continuar en varias tareas.
Crear tareas anidadas desasociadas
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 sincronizada con la tarea exterior de
cualquier manera especial. Tales tareas se llaman una tarea anidada aislada. En el siguiente ejemplo
se muestra una tarea que crea una tarea anidada desasociada.
Dim outer = Task.Factory.StartNew(Sub()
Console.WriteLine("Outer task beginning.")
Dim child = Task.Factory.StartNew(Sub()
Thread.SpinWait(5000000)
Console.WriteLine("Detached task completed.")
End Sub)
End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 20 de 107
' Output:
' Outer task beginning.
' Outer task completed.
' Detached child completed.
Observe que la tarea externa no espera a que la tarea anidada se complete.
Crear tareas secundarias
Cuando el código de usuario que se está ejecutando en una tarea crea una tarea con la opción
AttachedToParent, la nueva tarea es conocido como una tarea secundaria de la tarea para originar,
que es conocido como la 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:
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Parent task beginning.")
Dim child = Task.Factory.StartNew(Sub()
Thread.SpinWait(5000000)
Console.WriteLine("Attached child completed.")
End Sub,
TaskCreationOptions.AttachedToParent)
End Sub)
outer.Wait()
Console.WriteLine("Parent task completed.")
' Output:
' Parent task beginning.
' Attached child completed.
' Parent task completed.
Esperar tareas
El tipo System.Threading.Tasks.Task y el tipo System.Threading.Tasks.Task(Of TResult) proporcionan
varias sobrecargas del método Task(Of TResult).Wait y Task.Wait que le permiten esperar para que
una tarea 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.
Dim tasks() =
{
Task.Factory.StartNew(Sub() MethodA()),
Task.Factory.StartNew(Sub() MethodB()),
Task.Factory.StartNew(Sub() MethodC())
}
' Block until all tasks complete.
Task.WaitAll(tasks)
' Continue on this thread...
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 21 de 107
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.
Control de excepciones en tareas
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.
Cancelar 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.
La clase TaskFactory
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 modelo más común es StartNew, que crea e inicia una tarea en una sola instrucción.
Al crear las tareas de continuación a partir de varios antepasados, utilice el método
ContinueWhenAll o el método ContinueWhenAnyo sus equivalentes en la clase Task(Of
TResult).
Para encapsular los métodos BeginX y EndX del modelo de programación asincrónica en una
instancia de Task o Task(Of TResult), use los métodos FromAsync.
El objeto TaskFactory predeterminado es accesible como propiedad estática de la clase Task o de la
clase Task(Of 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.
Tareas sin delegados
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 22 de 107
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(Of 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.
Programadores personalizados
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.
Estructuras de datos relacionadas
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.
Los tipos Task personalizados
Recomendamos no heredar de System.Threading.Tasks.Task o System.Threading.Tasks.Task(Of
TResult). En su lugar, utilice la propiedad AsyncState para asociar los datos adicionales o decir con
objeto Task(Of TResult) o Task. También puede utilizar los métodos de extensión para extender la
funcionalidad de las clases Task(Of TResult) y Task.
Si debe heredar de Task o Task(Of TResult), no puede utilizar System.Threading.Tasks.TaskFactory, las
clases System.Threading.Tasks.TaskCompletionSource(Of TResult) o
System.Threading.Tasks.TaskFactory(Of TResult)para crear instancias de su tipo de tarea
personalizado porque estas clases sólo crean objetos Task(Of TResult) y Task. Además, no puede
utilizar los mecanismos de continuación de tarea que son proporcionados por Task, Task(Of TResult),
TaskFactory y TaskFactory(Of TResult) para crear instancias de su tipo de tarea personalizado porque
estos mecanismos también crean sólo objetos Task(Of TResult) y Task.
1.2.1. Tareas de continuación
En la programación asincrónica, es muy común que una operación asincrónica, cuando se completa,
invoque una segunda operación y le pase datos. Tradicionalmente, esto se hacía utilizando métodos
de devolución de llamada. En la biblioteca TPL (Task Parallel Library, biblioteca de procesamiento
paralelo basado en tareas), las tareas de continuación proporcionan la misma funcionalidad. Una
tarea de continuación es una tarea asincrónico (también se conoce así como una continuación) que
es invocada por otra tarea, que es conocido como el antecedente, cuando el antecedente completa.
Las continuaciones son relativamente fáciles de usar, y no por ello dejan de ser eficaces y flexibles.
Por ejemplo, puede:
Pasar datos del antecedente a la continuación
Especificar las condiciones precisas en las que se invocará o no la continuación
Cancelar una continuación antes de iniciarse o, de manera cooperativa, mientras se está
ejecutando
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 23 de 107
Proporcionar sugerencias sobre cómo se debería programar la continuación
Invocar varias continuaciones desde el mismo antecedente
Invocar una continuación cuando se completa una parte o la totalidad de los antecedentes
Encadenar las continuaciones una tras otra hasta cualquier longitud arbitraria
Usar una continuación para controlar las excepciones producidas por el antecedente
Las continuaciones se crean con el método Task.ContinueWith. En el siguiente ejemplo se muestra el
modelo básico (para mayor claridad, se omite el control de excepciones).
' The antecedent task. Can also be created with Task.Factory.StartNew.
Dim taskA As Task(Of DayOfWeek) = New Task(Of DayOfWeek)(Function()
Return DateTime.Today.DayOfWeek
End Function)
' The continuation. Its delegate takes the antecedent task as an argument and can return a different type.
Dim continuation As Task(Of String) = taskA.ContinueWith(Function(antecedent)
Return String.Format("Today is {0}", antecedent.Result)
End Function)
' Start the antecedent.
taskA.Start()
' Use the contuation's result.
Console.WriteLine(continuation.Result)
También puede crear una continuación de varias tareas que se ejecutará cuando una parte o la
totalidad de las tareas de una matriz de tareas se haya completado, como se muestra en el siguiente
ejemplo.
Dim task1 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 34
End Function)
Dim task2 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 8
End Function)
Dim tasks() As Task(Of Integer) = {task1, task2}
Dim continuation = Task.Factory.ContinueWhenAll(tasks, Sub(antecedents)
Dim answer As Integer = tasks(0).Result + tasks(1).Result
Console.WriteLine("The answer is {0}", answer)
End Sub)
task1.Start()
task2.Start()
continuation.Wait()
Una continuación se crea en el estado WaitingForActivation y, por lo tanto, únicamente puede
iniciarla su tarea antecedente. Al llamar a Task.Start en una continuación en el código de usuario, se
produce una excepción System.InvalidOperationException.
Una continuación se es Task y no bloquea el subproceso en el que se inicia. Utilice el método de
espera para bloquearse hasta que la tarea de continuación complete.
Opciones de una continuación
Al crear una continuación de una sola tarea, puede usar una sobrecarga ContinueWith que tome la
enumeración System.Threading.Tasks.TaskContinuationOptions para especificar las condiciones en
las que la tarea antecedente debe iniciar la continuación. Por ejemplo, puede especificar que la
continuación se ejecute solo si el antecedente se ejecutó completamente o solo si se completó con
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 24 de 107
errores, etc. Si no se cumple la condición cuando el antecedente está listo para invocar la
continuación, la continuación pasa directamente al estado Canceled y desde ese momento no se
podrá iniciar. Si especifica la opción NotOn u OnlyOn con una continuación de varias tareas, se
producirá una excepción en tiempo de ejecución.
La enumeración System.Threading.Tasks.TaskContinuationOptions también incluye las mismas
opciones que la enumeración System.Threading.Tasks.TaskCreationOptions. AttachedToParent,
LongRunning y PreferFairness tienen los mismos significados y valores en ambos tipos de
enumeración. Estas opciones se pueden usar con continuaciones de varias tareas.
En la siguiente tabla se muestran todos los valores de TaskContinuationOptions.
Elemento Descripción
None
Especifica el comportamiento predeterminado cuando no se especifican
TaskContinuationOptions. La continuación se programará una vez
completado el antecedente, independientemente del estado final de este.
Si la tarea es una tarea secundaria, se crea como una tarea anidada
desasociada.
PreferFairness
Especifica que la continuación se programará de modo que las tareas
programadas antes tengan más posibilidades de ejecutarse antes y las
tareas programadas después tengan más posibilidades de ejecutarse más
tarde.
LongRunning
Especifica que la continuación será una operación general de larga
duración. Proporciona una sugerencia al
System.Threading.Tasks.TaskScheduler de que se puede garantizar la
sobresuscripción.
AttachedToParent
Especifica que la continuación, si es una tarea secundaria, se adjunta a un
elemento primario en la jerarquía de tareas. La continuación es una tarea
secundaria solo si su antecedente también es una tarea secundaria.
NotOnRanToCompletion
Especifica que no se debe programar la continuación si su antecedente se
ejecuta completamente.
NotOnFaulted
Especifica que no se debe programar la continuación si su antecedente
produjo una excepción no controlada.
NotOnCanceled
Especifica que no se debe programar la continuación si se cancela su
antecedente.
OnlyOnRanToCompletion
Especifica que la continuación solo se debe programar si el antecedente se
ejecuta completamente.
OnlyOnFaulted
Especifica que la continuación solo se debe programar si su antecedente
produjo una excepción no controlada. Al utilizar la opción OnlyOnFaulted,
se garantiza que la propiedad Exception en el antecedente no es nula.
Puede usar esa propiedad para detectar la excepción y ver qué excepción
provocó el error de la tarea. Si no tiene acceso a la propiedad Exception, no
se controlará la excepción. Asimismo, si intenta tener acceso a la propiedad
Result de una tarea cancelada o con errores, se producirá una nueva
excepción.
OnlyOnCanceled
Especifica que la continuación debe programarse únicamente si su
antecedente se completa en estado Canceled.
ExecuteSynchronously
Para las continuaciones de muy corta duración. Especifica que lo ideal es
que la continuación se ejecute en el mismo subproceso que causa la
transición del antecedente a su estado final. Si el antecedente ya se ha
completado cuando se crea la continuación, el sistema intentará ejecutar la
continuación en el subproceso que la crea. Si CancellationTokenSource del
antecedente se dispone en un bloque finally (Finally en Visual Basic), una
continuación con esta opción se ejecutará en ese bloque finally.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 25 de 107
Pasar datos a una continuación
Una referencia al antecedente se pasa como argumento al delegado de usuario de la continuación. Si
el antecedente es System.Threading.Tasks.Task(Of TResult) y la tarea se ejecutó completamente, la
continuación puede tener acceso a la propiedad Task(Of TResult).Result de la tarea. Con una
continuación de varias tareas y el método Task.WaitAll, el argumento es la matriz de antecedentes. Al
usar Task.WaitAny, el argumento es el primer antecedente que se completó.
Task(Of TResult).Result se bloquea hasta que la tarea se ha completado. Sin embargo, si la tarea se
canceló o tiene errores, Result produce una excepción cuando el código intenta tener acceso al
mismo. Puede evitar este problema mediante la opción OnlyOnRanToCompletion, como se muestra
en el siguiente ejemplo.
Dim aTask = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim bTask = aTask.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {0}", antecedent.Result)
End Sub, TaskContinuationOptions.OnlyOnRanToCompletion)
Si desea que la continuación se ejecute aunque el antecedente no se ejecute completamente, debe
usar medidas de protección contra la excepción. Un posible enfoque es probar el estado del
antecedente e intentar tener acceso a Result solamente si el estado no es Faulted o Canceled.
También puede examinar la propiedad Exception del antecedente.
Cancelar una continuación
Una continuación pasa al estado Canceled en estos escenarios:
Cuando produce una excepción OperationCanceledException en respuesta a una solicitud de
cancelación. Al igual que sucede con cualquier tarea, si la excepción contiene el mismo token
que se pasó a la continuación, se trata como una confirmación de cancelación cooperativa.
Cuando la continuación se pasó System.Threading.CancellationToken como un argumento y
la propiedad IsCancellationRequested del símbolo es true (True) antes de la continuación se
ejecuta. En este caso, la continuación no se inicia y pasa directamente al estado Canceled.
Cuando la continuación nunca se ejecuta porque no se cumple la condición establecida en
TaskContinuationOptions. Por ejemplo, si una tarea entra en estado Faulted, su continuación,
creada con la opción NotOnFaulted, pasará al estado Canceled y no se ejecutará.
Para que una continuación no se ejecute si su antecedente se cancela, especifique la opción
NotOnCanceled al crear la continuación.
Si una tarea y su continuación representan dos partes de la misma operación lógica, puede pasar el
mismo token de cancelación a ambas tareas, como se muestra en el siguiente ejemplo.
Dim someCondition As Boolean = True
Dim cts As New CancellationTokenSource
Dim task1 = New Task(Sub()
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here...
End While
End Sub,
cts.Token
)
Dim task2 = task1.ContinueWith(Sub(antecedent)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 26 de 107
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here
End While
End Sub,
cts.Token)
task1.Start()
' Antecedent and/or continuation will
' respond to this request, depending on when it is made.
cts.Cancel()
Si el antecedente no se cancela, todavía se puede usar el token para cancelar la continuación. Si el
antecedente se cancela, no se inicia la continuación.
Después de que una continuación 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.
Continuaciones y tareas secundarias
Una continuación no se ejecuta hasta que se completan su antecedente y todas las tareas
secundarias asociadas. La continuación 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.
Controlar las excepciones que producen las continuaciones
Una relación entre un antecedente y una continuación no es una relación 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 continuación.
1. Use el método Wait, WaitAny o WaitAll, o su homólogo genérico, para esperar en la
continuación. Puede esperar a un antecedente y sus continuaciones en la misma instrucción
try (Try en Visual Basic), como se muestra en el siguiente ejemplo.
Dim task1 = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim continuation = task1.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {0}", antecedent.Result)
Throw New InvalidOperationException()
End Sub)
Try
task1.Wait()
continuation.Wait()
Catch ae As AggregateException
For Each ex In ae.InnerExceptions
Console.WriteLine(ex.Message)
Next
End Try
Console.WriteLine("Exception handled. Let's move on.")
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 27 de 107
1. Use una segunda continuación para observar la propiedad Exception de la primera
continuación.
2. Si la continuación es una tarea secundaria creada mediante la opción AttachedToParent, la
tarea primaria propagará sus excepciones al subproceso que realiza la llamada, como sucede
con cualquier otro elemento secundario asociado.
1.2.2. Tareas anidadas y tareas secundarias
Una tarea anidada no es más 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 opción AttachedToParent.
Una tarea puede crear cualquier número de tareas secundarias y anidadas, con la única limitación de
los recursos del sistema. En el ejemplo siguiente se muestra una tarea primaria que crea una tarea
anidada simple.
Shared Sub SimpleNestedTask()
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Outer task executing.")
Dim child = Task.Factory.StartNew(Sub()
Console.WriteLine("Nested task starting.")
Thread.SpinWait(500000)
Console.WriteLine("Nested task completing.")
End Sub)
End Sub)
parent.Wait()
Console.WriteLine("Outer task has completed.")
End Sub
' Sample output:
' Outer task executing.
' Nested task starting.
' Outer task has completed.
' Nested task completing.
Tareas secundarias asociadas frente a tareas anidadas desasociadas
El aspecto más 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 están estrechamente sincronizadas con el la tarea primaria. Si se modifica la
instrucción de creación de la tarea para usar la opción AttachedToParent, como se muestra en el
siguiente ejemplo,
Dim child = Task.Factory.StartNew(Sub()
Console.WriteLine("Attached child starting.")
Thread.SpinWait(5000000)
Console.WriteLine("Attached child completing.")
End Sub, TaskCreationOptions.AttachedToParent)
se generará el siguiente resultado.
' Parent task executing.
' Attached child starting.
' Attached child completing.
' Parent has completed.
Puede usar tareas secundarias asociadas para crear gráficos de operaciones asincrónicas con una
estrecha sincronización. Sin embargo, en la mayoría de los escenarios, recomendamos usar tareas
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 28 de 107
anidadas porque las relaciones con otras tareas son menos complejas. Esta es la razón por la que las
tareas que se crean dentro de otras tareas están anidadas de forma predeterminada y es necesario
especificar explícitamente la opción AttachedToParent para crear una tarea secundaria.
En la tabla siguiente se muestran las diferencias básicas entre los dos tipos de tareas secundarias.
Categoría
Tareas
anidadas
Tareas secundarias
asociadas
La tarea externa (primaria) espera a que las tareas internas se
completen.
No Sí
La tarea primaria propaga las excepciones iniciadas por las
tareas secundarias (tareas internas).
No Sí
El estado de la tarea primaria (tarea externa) depende del
estado de la tarea secundaria (tarea interna).
No Sí
En escenarios desasociados en los que la tarea anidada es un objetoTask(Of 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.
Shared Sub WaitForSimpleNestedTask()
Dim parent = Task(Of Integer).Factory.StartNew(Function()
Console.WriteLine("Outer task executing.")
Dim child = Task(Of Integer).Factory.StartNew(Function()
Console.WriteLine("Nested task starting.")
Thread.SpinWait(5000000)
Console.WriteLine("Nested task completing.")
Return 42
End Function)
Return child.Result
End Function)
Console.WriteLine("Outer has returned {0}", parent.Result)
End Sub
'Sample output:
' Outer task executing.
' Nested task starting.
' Detached task completing.
' Outer has returned 42
Excepciones en tareas anidadas y secundarias
Si una tarea anidada produce una excepción, debe observarse o controlarse directamente en la tarea
exterior como si se tratara de una tarea no anidada. Si un tiros del elemento secundario adjuntos una
excepción, la excepción se propaga automáticamente a la tarea primaria y atrás al subproceso que
espera o intenta tener 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.
Cancelación y tareas secundarias
No conviene olvidar que la cancelación de tareas es cooperativa. Por tanto, para ser "cancelable",
cada tarea secundaria asociada o desasociada debe supervisar el estado del token de cancelación. Si
desea cancelar un elemento primario y todos sus elementos secundarios utilizando una sola solicitud
de cancelación, debe pasar el mismo token como argumento a todas las tareas y proporcionar en
cada tarea la lógica de respuesta a la solicitud.
Cuando la tarea primaria se cancela
Si una tarea primaria se cancela antes de que se inicie una tarea secundaria, como es lógico, la tarea
secundaria (anidada) nunca se cancelará. Si una tarea primaria se cancela después de que se ha
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 29 de 107
iniciado una tarea secundaria o anidada, la tarea anidada (secundaria) se ejecutará hasta completarse
a menos que tenga su propia lógica de cancelación.
Cuando una tarea anidada se cancela
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 excepción, pues la excepción se
trata cono una cancelación de cooperación benigna. Este comportamiento es igual que el de
cualquier tarea de nivel superior.
Cuando una tarea secundaria se cancela
Cuando una tarea secundaria asociada se cancela usando el mismo token que se pasó a la tarea, se
propaga una excepción TaskCanceledException al subproceso de unión dentro de
AggregateException. Es muy importante esperar a la tarea primaria para poder controlar todas las
excepciones benignas además de todos las excepciones de error que se propagan de manera
ascendente a través de un gráfico de tareas secundarias asociadas.
1.2.3. Cancelación de tareas
Las clases System.Threading.Tasks.Task(Of TResult) y System.Threading.Tasks.Task admiten la
cancelación a través del uso de tokens de cancelación, que son una novedad de .NET Framework 4.
En las clases Task, la cancelación implica la cooperación entre el delegado de usuario, que representa
una operación cancelable y el código que solicitaron la cancelación. UNA cancelación correcta
implica el código para solicitar que llama al método CancellationTokenSource.Cancely el delegado de
usuario que finaliza la operación de una manera oportuna. Puede finalizar la operación a través de
una de estas opciones:
Devolver simplemente un valor del delegado. En muchos escenarios esto es suficiente; sin
embargo, una instancia de la tarea que está cancelada de esta manera las transiciones a
RanToCompletion dice, no al estado Cancelado.
Producir una excepción OperationCanceledException y pasarle el token en el que se solicitó
la cancelación. En este caso, se prefiere usar el método ThrowIfCancellationRequested. Una
tarea cancelada de esta manera cambia al estado Canceled, que sirve al código que realiza la
llamada para comprobar que la tarea respondió a su solicitud de cancelación.
En el siguiente ejemplo se muestra el modelo básico para la opción de cancelación de tareas que
produce la excepción. Observe que el token se pasa al delegado de usuario y a la propia instancia de
la tarea.
Imports System.Threading
Imports System.Threading.Tasks
Module Test
Sub Main()
Dim tokenSource2 As New CancellationTokenSource()
Dim ct As CancellationToken = tokenSource2.Token
Dim t2 = Task.Factory.StartNew(Sub()
' Were we already canceled?
ct.ThrowIfCancellationRequested()
Dim moreToDo As Boolean = True
While moreToDo = True
' Poll on this property if you have to do other cleanup before throwing.
If ct.IsCancellationRequested Then
' Clean up here, then...
ct.ThrowIfCancellationRequested()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 30 de 107
End If
End While
End Sub _
, tokenSource2.Token) ' Pass same token to StartNew.
' Cancel the task.
tokenSource2.Cancel()
' Just continue on this thread, or Wait/WaitAll with try-catch:
Try
t2.Wait()
Catch e As AggregateException
For Each item In e.InnerExceptions
Console.WriteLine(e.Message & " " & item.Message)
Next
End Try
Console.ReadKey()
End Sub
End Module
Cuando una instancia de tarea observa una excepción OperationCanceledException iniciada desde el
código de usuario, compara el token de la excepción con su token asociado (el que se pasó a la API
que creó la tarea). Si son los mismos y la propiedad IsCancellationRequested del símbolo devuelve
verdadero, la tarea interpreta esto como confirmar cancelación y transiciones al estado Cancelado. Si
no utiliza método WaitAll sino Wait para esperar por la tarea, a continuación, la tarea apenas
establece su estado en Canceled.
Si espera en una tarea que cambia al estado Canceled, se crea y se inicia una excepción
TaskCanceledException (encapsulada en AggregateException). Observe que esta excepción indica la
cancelación correcta en lugar de una situación 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 excepción no
coincide con el token de la tarea, OperationCanceledException se trata como una excepción normal,
por lo que la tarea cambia al estado Faulted. Observe también que la presencia de otras excepciones
también 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 continúe procesando algunos elementos una vez solicitada la cancelación.

1.2.4. Control de excepciones (Task Parallel Library)
Las excepciones no controladas que se inician mediante el código de usuario que se ejecuta dentro
de una tarea se propagan de nuevo al subproceso de unión, excepto en determinados escenarios
que se describen posteriormente en este tema. Se propagan Excepciones al utilizar uno de los
métodos Task(Of TResult).Wait o estática o instancia Task.Wait, y los administra agregando la llamada
en una instrucción de 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
excepción, se encapsulará en un objeto AggregateException.
Dim task1 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("I'm bad, but not too bad!")
End Sub)
Try
task1.Wait()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 31 de 107
Catch ae As AggregateException
' 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.
For Each ex In ae.InnerExceptions
If TypeOf (ex) Is MyCustomException Then
Console.WriteLine(ex.Message)
Else
Throw
End If
Next
End Try
Para evitar una excepción no controlada, basta con detectar el objeto AggregateException y omitir
las excepciones internas. Sin embargo, esta operación no resulta recomendable porque es igual que
detectar el tipo Exception base en escenarios no paralelos. Si desea detectar una excepción sin
realizar acciones concretas que la resuelvan, puede dejar al programa en un estado indeterminado.
Si no espera que ninguna tarea propague la excepción ni tiene acceso a su propiedad Exception, la
excepción 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 unión, es posible que una
tarea continúe procesando algunos elementos después de que se haya producido la excepción.
Tareas secundarias asociadas y objetos AggregateException anidados
Si una tarea tiene una tarea secundaria adjunta que inicia una excepción, esa excepción se encapsula
en un objeto AggregateException antes de que se propague a la tarea primaria, que encapsula esa
excepción en su propio objeto AggregateException antes de propagarla de nuevo al subproceso que
realiza la llamada. En casos como éste, la AggregateException.InnerExceptions propiedad de
AggregateException que se detecta en Task.Wait u Task(Of TResult).Wait u WaitAny o el método
WaitAll contiene uno o más AggregateException crea instancias, no las excepciones originales que
produjeron el error. Para evitar tener que iterar sobre AggregateExceptions anidado, puede utilizar el
método Flatten para quitar todo el AggregateExceptions anidado, para que la AggregateException
InnerExceptions propiedad contenga las excepciones originales. En el siguiente ejemplo, las
instancias AggregateException anidadas se quitan información de estructura jerárquica y administran
en sólo un bucle.
' task1 will throw an AE inside an AE inside an AE
Dim task1 = Task.Factory.StartNew(Sub()
Dim child1 = Task.Factory.StartNew(Sub()
Dim child2 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("Attached child2 faulted.")
End Sub,
TaskCreationOptions.AttachedToParent)
End Sub,
TaskCreationOptions.AttachedToParent)
' Uncomment this line to see the exception rethrown. throw new MyCustomException("Attached child1 faulted.")
End Sub)
Try
task1.Wait()
Catch ae As AggregateException
For Each ex In ae.Flatten().InnerExceptions
If TypeOf (ex) Is MyCustomException Then
Console.WriteLine(ex.Message)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 32 de 107
Else
Throw
End If
Next
'or like this:
' ae.Flatten().Handle(Function(e)
' Return TypeOf (e) Is MyCustomException
' End Function)
End Try
Excepciones de tareas secundarias desasociadas
De forma predeterminada, las tareas secundarias están 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 excepción
de una tarea desasociada para encapsularla en un objeto AggregateException y propagarla de nuevo
al subproceso de unión.
Dim task1 = Task.Factory.StartNew(Sub()
Dim nestedTask1 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("Nested task faulted.")
End Sub)
' Here the exception will be escalated back to joining thread.
' We could use try/catch here to prevent that.
nestedTask1.Wait()
End Sub)
Try
task1.Wait()
Catch ae As AggregateException
For Each ex In ae.Flatten().InnerExceptions
If TypeOf (ex) Is MyCustomException Then
' Recover from the exception. Here we just
' print the message for demonstration purposes.
Console.WriteLine(ex.Message)
End If
Next
End Try
Aunque se use una tarea de continuación para observar una excepción en una tarea secundaria, la
tarea primaria debe seguir observando la excepción.
Excepciones que indican la cancelación cooperativa
Cuando el código de usuario de una tarea responde a una solicitud de cancelación, el procedimiento
correcto es producir una excepción OperationCanceledException que se pasa en el token de
cancelación con el que se comunicó la solicitud. Antes de intentar propagar la excepción, la instancia
de la tarea compara el token de la excepción con el que recibió durante su creación. Si son iguales, la
tarea propaga una excepción TaskCanceledException encapsulada en un elemento
AggregateException y puede verse cuando se examinan las excepciones internas. Sin embargo, si el
subproceso de unión no está esperando la tarea, no se propagará esta excepción concreta.
Dim someCondition As Boolean = True
Dim tokenSource = New CancellationTokenSource()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 33 de 107
Dim token = tokenSource.Token
Dim task1 = Task.Factory.StartNew(Sub()
Dim ct As CancellationToken = token
While someCondition = True
' Do some work...
Thread.SpinWait(500000)
ct.ThrowIfCancellationRequested()
End While
End Sub,
token)
Usar el método Handle para filtrar excepciones internas
El método Handle puede usarse para filtrar excepciones que pueden tratarse como "controladas" sin
necesidad de usar ninguna otra lógica. En el delegado de usuario que se proporciona a Handle, se
puede examinar el tipo de excepción, su propiedad Message o cualquier otra información sobre esta
excepción que permita determinar si es benigna. Las excepciones en las que el delegado devuelve
false se reinician inmediatamente en una nueva instancia de AggregateException después de que
Handle devuelve un valor.
En el siguiente fragmento de código se usa un bucle foreach sobre las excepciones internas.
For Each ex In ae.InnerExceptions
If TypeOf (ex) Is MyCustomException Then
Console.WriteLine(ex.Message)
Else
Throw
End If
Next
En el siguiente fragmento de código se muestra el uso del método Handle con la misma función.
ae.Handle(Function(ex)
Return TypeOf (ex) Is MyCustomException
End Function)
Observar excepciones mediante la propiedad Task.Exception
Si una tarea completa en el estado Faulted, su propiedad Exception se puede examinar para detectar
qué excepción concreta produjo el error. Un mecanismo adecuado para observar la propiedad
Exception es usar una continuación que se ejecute solo si se produce un error en la tarea anterior, tal
y como se muestra en el siguiente ejemplo.
Dim task1 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("task1 faulted.")
End Sub).ContinueWith(Sub(t)
Console.WriteLine("I have observed a {0}",
t.Exception.InnerException.GetType().Name)
End Sub, TaskContinuationOptions.OnlyOnFaulted)
En una aplicación real, el delegado de continuación podría registrar información detallada sobre la
excepción y posiblemente generar nuevas tareas para recuperarse de la excepción.
Evento UnobservedTaskException
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 34 de 107
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 difícil
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 excepción no observada se propague de nuevo al subproceso de unión.
1.2.5. Cómo: Usar Parallel.Invoke para ejecutar operaciones paralelas
En este ejemplo se muestra cómo paralelizar las operaciones utilizando ParallelInvoke en la biblioteca
TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas). En un origen de
datos compartido se realizan tres operaciones. Dado que ninguna de ellas modifica el origen, se
pueden ejecutar en paralelo de manera sencilla.
Ejemplo
' How to: Use Parallel.Invoke to Execute Parallel Operations
Option Explicit On
Option Strict On
Imports System.Threading.Tasks
Imports System.Net
Module ParallelTasks
Sub Main()
' Retrieve Darwin's "Origin of the Species" from Gutenberg.org.
Dim words As String() = CreateWordArray("http://www.gutenberg.org/files/2009/2009.txt")
'#Region "ParallelTasks"
' Perform three tasks in parallel on the source array
Parallel.Invoke(Sub()
Console.WriteLine("Begin first task...")
GetLongestWord(words)
' close first Action
End Sub,
Sub()
Console.WriteLine("Begin second task...")
GetMostCommonWords(words)
'close second Action
End Sub,
Sub()
Console.WriteLine("Begin third task...")
GetCountForWord(words, "species")
'close third Action
End Sub)
'close parallel.invoke
Console.WriteLine("Returned from Parallel.Invoke")
'#End Region
Console.WriteLine("Press any key to exit")
Console.ReadKey()
End Sub
#Region "HelperMethods"
Sub GetCountForWord(ByVal words As String(), ByVal term As String)
Dim findWord = From word In words _
Where word.ToUpper().Contains(term.ToUpper()) _
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 35 de 107
Select word
Console.WriteLine("Task 3 -- The word ""{0}"" occurs {1} times.", term, findWord.Count())
End Sub
Sub GetMostCommonWords(ByVal words As String())
Dim frequencyOrder = From word In words _
Where word.Length > 6 _
Group By word
Into wordGroup = Group, Count()
Order By wordGroup.Count() Descending _
Select wordGroup
Dim commonWords = From grp In frequencyOrder
Select grp
Take (10)
Dim s As String
s = "Task 2 -- The most common words are:" & vbCrLf
For Each v In commonWords
s = s & v(0) & vbCrLf
Next
Console.WriteLine(s)
End Sub
Function GetLongestWord(ByVal words As String()) As String
Dim longestWord = (From w In words _
Order By w.Length Descending _
Select w).First()
Console.WriteLine("Task 1 -- The longest word is {0}", longestWord)
Return longestWord
End Function
' An http request performed synchronously for simplicity.
Function CreateWordArray(ByVal uri As String) As String()
Console.WriteLine("Retrieving from {0}", uri)
' Download a web page the easy way.
Dim s As String = New WebClient().DownloadString(uri)
' Separate string into an array of words, removing some common punctuation.
Return s.Split(New Char() {" "c, ControlChars.Lf, ","c, "."c, ";"c, ":"c, _
"-"c, "_"c, "/"c}, StringSplitOptions.RemoveEmptyEntries)
End Function
#End Region
' Output (May vary on each execution):
' Retrieving from http://www.gutenberg.org/dirs/etext99/otoos610.txt
' Response stream received.
' Begin first task...
' Begin second task...
' Task 2 -- The most common words are:
' species
' selection
' varieties
' natural
' animals
' between
' different
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 36 de 107
' distinct
' several
' conditions
' Begin third task...
' Task 1 -- The longest word is characteristically
' Task 3 -- The word "species" occurs 1927 times.
' Returned from Parallel.Invoke
' Press any key to exit
End Module
Observe que con Invoke, simplemente expresa qué acciones desea que se ejecuten simultáneamente;
el runtime controla todos los detalles de programación de subprocesos, incluido el escalado
automático al número de núcleos del equipo host.
En este ejemplo se paralelizan las operaciones, no los datos. Como enfoque alternativo, puede
paralelizar los consultas LINQ mediante PLINQ y ejecutar las consultas de forma secuencial. También
puede paralelizar los datos con PLINQ. Otra opción consiste en paralelizar las consultas y las tareas.
Aunque la sobrecarga resultante podría degradar el rendimiento en equipos host con relativamente
pocos procesadores, se ajustaría mucho mejor en equipos con muchos procesadores.

1.2.6. Cómo: Devolver un valor de una tarea
En este ejemplo se muestra cómo se usa el tipo System.Threading.Tasks.Task(Of TResult) para
devolver un valor de la propiedad Result.
Ejemplo
' How to: Return a Value from a Task
Imports System.Threading.Tasks
Module Module1
Sub Main()
ReturnAValue()
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Sub ReturnAValue()
' Return a value type with a lambda expression
Dim task1 = Task(Of Integer).Factory.StartNew(Function() 1)
Dim i = task1.Result
' Return a named reference type with a multi-line statement lambda.
Dim task2 As Task(Of Test) = Task.Factory.StartNew(Function()
Dim s As String = ".NET"
Dim d As Integer = 4.0
Return New Test With {.Name = s, .Number = d}
End Function)
Dim myTest As Test = task2.Result
Console.WriteLine(myTest.Name & ":" & myTest.Number)
' Return an array produced by a PLINQ query
Dim task3 As Task(Of String())= Task(Of String()).Factory.StartNew(Function()
Dim path = "C:\Users\Public\Pictures\Sample Pictures\"
Dim files = System.IO.Directory.GetFiles(path)
Dim result = (From file In files.AsParallel()
Let info = New System.IO.FileInfo(file)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 37 de 107
Where info.Extension = ".jpg"
Select file).ToArray()
Return result
End Function)
For Each name As String In task3.Result
Console.WriteLine(name)
Next
End Sub
Class Test
Public Name As String
Public Number As Double
End Class
End Module
La propiedad Result bloquea el subproceso que realiza la llamada hasta que la tarea finaliza.

1.2.7. Cómo: Esperar a que una o varias tareas se completen
En este ejemplo se muestra cómo utilizar el método Wait o su equivalente en la clase Task(Of
TResult), para esperar en una tarea única. También se muestra cómo utilizar los métodos WaitAny y
WaitAll estáticos para esperar en varias tareas.
Ejemplo
' How to: Wait on One or More Tasks to Complete
Imports System.Threading
Imports System.Threading.Tasks
Module WaitOnTasks
Dim rand As New Random()
Sub Main()
' Wait on a single task with no timeout specified.
Dim taskA = Task.Factory.StartNew(Sub() DoSomeWork(10000000))
taskA.Wait()
Console.WriteLine("taskA has completed.")
' Wait on a single task with a timeout specified.
Dim taskB = Task.Factory.StartNew(Sub() DoSomeWork(10000000))
taskB.Wait(100) 'Wait for 100 ms.
If (taskB.IsCompleted) Then
Console.WriteLine("taskB has completed.")
Else
Console.WriteLine("Timed out before task2 completed.")
End If
' Wait for all tasks to complete.
Dim myTasks(9) As Task
For i As Integer = 0 To myTasks.Length - 1
myTasks(i) = Task.Factory.StartNew(Sub() DoSomeWork(10000000))
Next
Task.WaitAll(myTasks)
' Wait for first task to complete.
Dim tasks2(2) As Task(Of Double)
' Try three different approaches to the problem. Take the first one.
tasks2(0) = Task(Of Double).Factory.StartNew(Function() TrySolution1())
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 38 de 107
tasks2(1) = Task(Of Double).Factory.StartNew(Function() TrySolution2())
tasks2(2) = Task(Of Double).Factory.StartNew(Function() TrySolution3())
Dim index As Integer = Task.WaitAny(tasks2)
Dim d As Double = tasks2(index).Result
Console.WriteLine("task(0) completed first with result of {1}.", index, d)
Console.ReadKey()
End Sub
' Dummy Functions to Simulate Work
Function DoSomeWork(ByVal val As Integer)
' Pretend to do something.
Thread.SpinWait(val)
End Function
Function TrySolution1()
Dim i As Integer = rand.Next(1000000)
' Simulate work by spinning
Thread.SpinWait(i)
Return i
End Function
Function TrySolution2()
Dim i As Integer = rand.Next(1000000)
' Simulate work by spinning
Thread.SpinWait(i)
Return i
End Function
Function TrySolution3()
Dim i As Integer = rand.Next(1000000)
' Simulate work by spinning
Thread.SpinWait(i)
Thread.SpinWait(1000000)
Return i
End Function
End Module
Por razones de simplicidad, estos ejemplos no muestran el código de control de excepciones ni el
código de cancelación. En la mayoría de los casos, debe incluir un método Wait en un bloque try-
catch, porque la espera es el mecanismo por el que el código 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.

1.2.8. Cómo: Cancelar una tarea y sus elementos secundarios
En estos ejemplos se muestra cómo realizar las tareas siguientes:
1. Crear e iniciar una tarea cancelable.
2. Pasar un token de cancelación a un delegado de usuario y, opcionalmente, a la instancia de
la tarea.
3. Observar y responder a la solicitud de cancelación en el delegado de usuario.
4. Opcionalmente, observar en el subproceso que realiza la llamada que la tarea se canceló.
El subproceso que realiza la llamada no finaliza la tarea forzosamente, sino que solo señala que se
solicita la cancelación. Si la tarea ya se está ejecutando, es el delegado de usuario el que debe
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 39 de 107
observar la solicitud y responder según corresponda. Si la cancelación se solicita antes de ejecutarse
la tarea, el delegado de usuario nunca se ejecuta y el objeto de tarea pasa al estado Cancelado.
Ejemplo
En este ejemplo se muestra cómo finalizar un objeto Task y sus elementos secundarios en respuesta
a una solicitud de cancelación. También se muestra que, cuando un delegado de usuario finaliza con
una excepción OperationCanceledException, el subproceso que realiza la llamada puede usar
opcionalmente el método Wait o el método 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.
' How to: Cancel a Task and Its Children
Imports System.Threading
Imports System.Threading.Tasks
Module CancelATask
Sub Main()
Console.WriteLine("Press any key to start. Press 'c' to cancel.")
Console.ReadKey()
Dim tokenSource As New CancellationTokenSource()
Dim token As CancellationToken = tokenSource.Token
' Store references to the tasks so that we can wait on them and observe their status after cancellation.
Dim tasks(10) As Task
' 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(Sub() 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(Sub()
' Create some cancelable child tasks.
For i As Integer = 2 To 10
' For each child task, pass the same token to each user delegate and to StartNew.
tasks(i) = Task.Factory.StartNew(Sub(iteration) DoSomeWork(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)
Next
End Sub _
, token)
' Give the tasks a second to start.
Thread.Sleep(1000)
' Request cancellation from the UI thread.
If Console.ReadKey().KeyChar = "c"c Then
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.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 40 de 107
End If
Try
Task.WaitAll(tasks)
Catch e As AggregateException
' For demonstration purposes, show the OCE message.
For Each v In e.InnerExceptions
Console.WriteLine("msg: " + v.Message)
Next
End Try
' Prove that the tasks are now all in a canceled state.
For i As Integer = 0 To tasks.Length
Console.WriteLine("task(0) status is now 1", i, tasks(i).Status)
Next
' Keep the console window open while the task completes its output.
Console.ReadLine()
End Sub
Sub DoSomeWork(ByVal taskNum As Integer, ByVal ct As CancellationToken)
' Was cancellation already requested?
If ct.IsCancellationRequested = True Then
Console.WriteLine("We were cancelled before we got started.")
Console.WriteLine("Press Enter to quit.")
ct.ThrowIfCancellationRequested()
End If
Dim maxIterations As Integer = 1000
' NOTE!!! An "OperationCanceledException was unhandled by user code" error will be raised here if "Just My
Code"
' is enabled on your computer. On Express editions JMC is enabled and cannot be disabled. The exception is
benign.
' Just press F5 to continue executing your code.
For i As Integer = 0 To maxIterations
' Do a bit of work. Not too much.
Dim sw As New SpinWait()
For j As Integer = 0 To 3000
sw.SpinOnce()
Next
Console.WriteLine("...0 ", taskNum)
If ct.IsCancellationRequested Then
Console.WriteLine("bye from 0.", taskNum)
Console.WriteLine("\nPress Enter to quit.")
ct.ThrowIfCancellationRequested()
End If
Next
End Sub
End Module
La clase System.Threading.Tasks.Task está totalmente integrada con el modelo de cancelación
basado en los tipos System.Threading.CancellationToken y
System.Threading.CancellationTokenSource.

1.2.9. Cómo: Controlar excepciones iniciadas por tareas
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 41 de 107
En los siguientes ejemplos, se muestra cómo controlar las excepciones producidas en una o varias
tareas.
Ejemplo
En este primer ejemplo, se detecta la excepción System.AggregateException y, a continuación, se
examina su propiedad AggregateExceptionInnerExceptions para determinar si algunas de las
excepciones se pueden controlar mediante el código del programa.
' How to: Handle Exceptions Thrown by Tasks
Imports System.Threading.Tasks
Module TaskExceptions
Function GetAllFiles(ByVal str As String) As String()
' Should throw an AccessDenied exception on Vista or later. If you see an "Exception was unhandled
' by user code" error, this is because "Just My Code" is enabled. Press F5 to continue execution or
' disable Just My Code.
Return System.IO.Directory.GetFiles(str, "*.txt", System.IO.SearchOption.AllDirectories)
End Function
Sub Main()
HandleExceptions()
' RethrowAllExceptions()
Console.WriteLine("Press any key.")
Console.ReadKey()
End Sub
Sub HandleExceptions()
' Assume this is a user-entered String.
Dim path = "C:\"
' Use this line to throw UnauthorizedAccessException, which we handle.
Dim task1 = Task(Of String()).Factory.StartNew(Function() GetAllFiles(path))
' Use this line to throw an exception that is not handled.
' Task task1 = Task.Factory.StartNew(Sub () throw new IndexOutOfRangeException() )
Try
task1.Wait()
Catch ae As AggregateException
ae.Handle(Function(x)
If TypeOf (x) Is UnauthorizedAccessException Then ' 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
Else
Return False ' Let anything else stop the application.
End If
End Function)
End Try
Console.WriteLine("task1 has completed.")
End Sub
Function GetValidExtensions(ByVal path As String) As String()
If path = "C:\" Then
Throw New ArgumentException("The system root is not a valid path.")
End If
Dim result(10) As String
Return result
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 42 de 107
End Function
Sub RethrowAllExceptions()
' Assume this is a user-entered String.
Dim path = "C:\"
Dim myTasks(2) As Task(Of String())
myTasks(0) = Task(Of String()).Factory.StartNew(Function() GetAllFiles(path))
myTasks(1) = Task(Of String()).Factory.StartNew(Function() GetValidExtensions(path))
myTasks(2) = Task(Of String()).Factory.StartNew(Function()
Dim s(10) As String
Return s
End Function)
Try
Task.WaitAll(myTasks)
Catch ae As AggregateException
Throw ae.Flatten()
End Try
Console.WriteLine("task1 has completed.")
End Sub
End Module
En este ejemplo, se detecta la excepción System.AggregateException, pero no se intenta controlar
ninguna de sus excepciones internas. En su lugar, se utiliza el método Flatten para extraer las
excepciones internas de todas las instancias anidadas de AggregateException y volver a iniciar una
sola excepción AggregateException que contiene directamente todas las excepciones internas no
controladas. Al reducir la excepción, resulta más fácil controlarla mediante código de cliente.
Imports System.Threading.Tasks
Module TaskExceptions2
Sub Main()
RethrowAllExceptions()
End Sub
Function GetAllFiles(ByVal str As String) As String()
' Should throw an AccessDenied exception on Vista or later. If you see an "Exception was unhandled
' by user code" error, this is because "Just My Code" is enabled. Press F5 to continue execution or
' disable Just My Code.
Return System.IO.Directory.GetFiles(str, "*.txt", System.IO.SearchOption.AllDirectories)
End Function
Function GetValidExtensions(ByVal path As String) As String()
If path = "C:\" Then
Throw New ArgumentException("The system root is not a valid path.")
End If
Dim result(10) As String
Return result
End Function
Sub RethrowAllExceptions()
' Assume this is a user-entered String.
Dim path = "C:\"
Dim myTasks(2) As Task(Of String())
myTasks(0) = Task(Of String()).Factory.StartNew(Function() GetAllFiles(path))
myTasks(1) = Task(Of String()).Factory.StartNew(Function() GetValidExtensions(path))
myTasks(2) = Task(Of String()).Factory.StartNew(Function()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 43 de 107
Dim s(10) As String
Return s
End Function)
Try
Task.WaitAll(myTasks)
Catch ae As AggregateException
Throw ae.Flatten()
End Try
Console.WriteLine("task1 has completed.")
End Sub
End Module
Para ejecutar este ejemplo, pegue el código en el ejemplo anterior y llame a RethrowAllExceptions
desde el método Main.

1.2.10. Cómo: Encadenar varias tareas con continuaciones
In the Task Parallel Library, a task whose ContinueWith method is invoked is called the antecedent
task and the task that is defined in the ContinueWith method is called the continuation. En este
ejemplo se muestra cómo utilizar los métodos ContinueWith y ContinueWith de las clases Task(Of
TResult) y Task para especificar una tarea que se inicia después de que sus finales de la tarea
antecedentes.
También muestra cómo especificar una continuación que solo se ejecuta si la tarea antecedente se
cancela.
En estos ejemplos se muestra cómo continuar desde una tarea única. También puede crear una
continuación que se ejecute después de que alguno o todos los grupos de tareas se completen o se
cancelen.
Ejemplo
En el método DoSimpleContinuation, se muestra la sintaxis básica de ContinueWith. Observe que la
tarea antecedente se proporciona como el parámetro de entrada a la expresión lambda en el método
ContinueWith. Esto le permite evaluar el estado de la tarea antecedente antes de realizar cualquier
trabajo en la continuación. Utilice esta sobrecarga simple de ContinueWith cuando no tenga que
pasar un estado de una tarea a otra.
En el método DoSimpleContinuationWithState, se muestra cómo utilizar ContinueWith para pasar el
resultado de la tarea antecedente a la tarea de continuación.
Imports System.IO
Imports System.Threading
Imports System.Threading.Tasks
Module ContinueWith
Sub Main()
DoSimpleContinuation()
Console.WriteLine("Press any key to exit")
Console.ReadKey()
End Sub
Sub DoSimpleContinuation()
Dim path As String = "C:\users\public\TPLTestFolder\"
Try
Dim firstTask = New Task(Sub() CopyDataIntoTempFolder(path))
Dim secondTask = firstTask.ContinueWith(Sub(t) CreateSummaryFile(path))
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 44 de 107
firstTask.Start()
Catch e As AggregateException
Console.WriteLine(e.Message)
End Try
End Sub
' A toy function to simulate a workload
Sub CopyDataIntoTempFolder(ByVal path__1 As String)
System.IO.Directory.CreateDirectory(path__1)
Dim rand As New Random()
For x As Integer = 0 To 49
Dim bytes As Byte() = New Byte(999) {}
rand.NextBytes(bytes)
Dim filename As String = Path.GetRandomFileName()
Dim filepath As String = Path.Combine(path__1, filename)
System.IO.File.WriteAllBytes(filepath, bytes)
Next
End Sub
Sub CreateSummaryFile(ByVal path__1 As String)
Dim files As String() = System.IO.Directory.GetFiles(path__1)
Parallel.ForEach(files, Sub(file)
Thread.SpinWait(5000)
End Sub)
System.IO.File.WriteAllText(Path.Combine(path__1, "__SummaryFile.txt"), "did my work")
Console.WriteLine("Done with task2")
End Sub
Sub DoSimpleContinuationWithState()
Dim nums As Integer() = {19, 17, 21, 4, 13, 8, _
12, 7, 3, 5}
Dim f0 = New Task(Of Double)(Function() nums.Average())
Dim f1 = f0.ContinueWith(Function(t) GetStandardDeviation(nums, t.Result))
f0.Start()
Console.WriteLine("the standard deviation is {0}", f1)
End Sub
Function GetStandardDeviation(ByVal values As Integer(), ByVal mean As Double) As Double
Dim d As Double = 0.0R
For Each n In values
d += Math.Pow(mean - n, 2)
Next
Return Math.Sqrt(d / (values.Length - 1))
End Function
End Module
El parámetro de tipo de Task(Of TResult) determina el tipo devuelto del delegado. Ese valor devuelto
se pasa a la tarea de continuación. Es posible encadenar un número arbitrario de tareas de esta
manera.

1.2.11. Cómo: Recorrer un árbol binario con tareas paralelas
En el siguiente ejemplo se muestran dos maneras de usar tareas paralelas para atravesar una
estructura de datos en árbol. La creación del propio árbol se deja como ejercicio.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 45 de 107
Ejemplo
Imports System.Threading.Tasks
Public Class TreeWalk
Shared Sub Main()
Dim tree As Tree(Of Person) = New Tree(Of Person)()
' ...populate tree (left as an exercise)
' Define the Action to perform on each node.
Dim myAction As Action(Of Person) = New Action(Of Person)(Sub(x)
Console.WriteLine("{0} : {1} ", x.Name, x.Number)
End Sub)
' Traverse the tree with parallel tasks.
DoTree(tree, myAction)
End Sub
Public Class Person
Public Name As String
Public Number As Integer
End Class
Public Class Tree(Of T)
Public Left As Tree(Of T)
Public Right As Tree(Of T)
Public Data As T
End Class
' By using tasks explicitly.
Public Shared Sub DoTree(Of T)(ByVal myTree As Tree(Of T), ByVal a As Action(Of T))
If Not myTree Is Nothing Then
Return
End If
Dim left = Task.Factory.StartNew(Sub() DoTree(myTree.Left, a))
Dim right = Task.Factory.StartNew(Sub() DoTree(myTree.Right, a))
a(myTree.Data)
Try
Task.WaitAll(left, right)
Catch ae As AggregateException
'handle exceptions here
End Try
End Sub
' By using Parallel.Invoke
Public Shared Sub DoTree2(Of T)(ByVal myTree As Tree(Of T), ByVal myAct As Action(Of T))
If Not myTree Is Nothing Then
Return
End If
Parallel.Invoke(
Sub() DoTree2(myTree.Left, myAct),
Sub() DoTree2(myTree.Left, myAct),
Sub() myAct(myTree.Data)
)
End Sub
End Class
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 46 de 107
Los dos métodos mostrados son equivalentes desde el punto de vista funcional. Cuando se usa el
método StartNew para crear y ejecutar las tareas, estas devuelven un identificador que se puede usar
para esperar en ellas y controlar las excepciones.

1.2.12. Cómo: Desencapsular una tarea anidada
Puede devolver una tarea de un método y esperar o continuar a partir de esa tarea, como se muestra
en el siguiente ejemplo:
Shared Function DoWorkAsync() As Task(Of String)
Return Task(Of String).Factory.StartNew(Function()
'...
Return "Work completed."
End Function)
End Function
Shared Sub StartTask()
Dim t As Task(Of String) = DoWorkAsync()
t.Wait()
Console.WriteLine(t.Result)
End Sub
En el ejemplo anterior, la propiedad Result es del tipo string (String en Visual Basic).
Sin embargo, en algunos casos le podría interesar crear una tarea dentro de otra y devolver la tarea
anidada. En este caso, TResult de la tarea que incluye es, en sí misma, una tarea. En el siguiente
ejemplo, la propiedad Result es Task<Task<string>> en C# o Task(Of Task(Of String)) en Visual Basic.
' Note the type of t and t2.
Dim t As Task(Of Task(Of String)) = Task.Factory.StartNew(Function() DoWorkAsync())
Dim t2 As Task(Of Task(Of String)) = DoWorkAsync().ContinueWith(Function(s) DoMoreWorkAsync())
' Outputs: System.Threading.Tasks.Task`1[System.String]
Console.WriteLine(t.Result)
Aunque es posible para escribir el código para desempaquetar la tarea exterior y recuperar la tarea
original y su propiedad Result, tal código no es fácil de escribir porque debe administrar las
excepciones y también las solicitud de la cancelación. En esta situación, recomendamos utilizar uno
de los métodos de extensión Unwrap, como se muestra en el siguiente ejemplo.
' Unwrap the inner task.
Dim t3 As Task(Of String) = DoWorkAsync().ContinueWith(Function(s) DoMoreWorkAsync()).Unwrap()
' Outputs "More work completed."
Console.WriteLine(t.Result)
Los métodos Unwrap se pueden utilizar para transformar Task<Task> o Task<Task<TResult>>
(Task(Of Task) o Task(Of Task(Of TResult)) en Visual Basic) en Task o Task<TResult> (Task(Of TResult)
en Visual Basic). La nueva tarea representa al completo la tarea anidada interna e incluye el estado de
cancelación y todas las excepciones.
Ejemplo
En el ejemplo siguiente se muestra cómo usar los métodos de extensión Unwrap.
'How to: Unwrap a Task
Imports System.Threading
Imports System.Threading.Tasks
Module UnwrapATask2
Sub Main()
' An arbitrary threshold value.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 47 de 107
Dim threshold As Byte = &H40
' myData is a Task(Of Byte())
Dim myData As Task(Of Byte()) = Task.Factory.StartNew(Function()
Return GetData()
End Function)
' We want to return a task so that we can continue from it later in the program.
' Without Unwrap: stepTwo is a Task(Of Task(Of Byte)) With Unwrap: stepTwo is a Task(Of Byte)
Dim stepTwo = myData.ContinueWith(Function(antecedent)
Return Task.Factory.StartNew(Function()
Return Compute(antecedent.Result)
End Function)
End Function).Unwrap()
Dim lastStep = stepTwo.ContinueWith(Function(antecedent)
Console.WriteLine("Result = {0}", antecedent.Result)
If antecedent.Result >= threshold Then
Return Task.Factory.StartNew(Sub()
Console.WriteLine("Program complete. Final = &H{1:x} threshold =
&H{1:x}",
stepTwo.Result, threshold)
End Sub)
Else
Return DoSomeOtherAsynchronousWork(stepTwo.Result, threshold)
End If
End Function)
Try
lastStep.Wait()
Catch ae As AggregateException
For Each ex As Exception In ae.InnerExceptions
Console.WriteLine(ex.Message & ex.StackTrace & ex.GetBaseException.ToString())
Next
End Try
Console.WriteLine("Press any key")
Console.ReadKey()
End Sub
#Region "Dummy_Methods"
Function GetData() As Byte()
Dim rand As Random = New Random()
Dim bytes(64) As Byte
rand.NextBytes(bytes)
Return bytes
End Function
Function DoSomeOtherAsynchronousWork(ByVal i As Integer, ByVal b2 As Byte) As Task
Return Task.Factory.StartNew(Sub()
Thread.SpinWait(500000)
Console.WriteLine("Doing more work. Value was <= threshold.")
End Sub)
End Function
Function Compute(ByVal d As Byte()) As Byte
Dim final As Byte = 0
For Each item As Byte In d
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 48 de 107
final = final Xor item
Console.WriteLine("{0:x}", final)
Next
Console.WriteLine("Done computing")
Return final
End Function
#End Region
End Module

1.3. TPL con otros modelos asincrónicos
La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas) se
puede usar con modelos tradicionales de programación asincrónica de .NET de varias maneras.
1.3.1. TPL y la programación asincrónica tradicional de .NET
.NET Framework proporciona los siguientes dos modelos estándar para realizar las operaciones
asincrónicas enlazadas a E/S y enlazadas a cálculos:
Modelo de programación asincrónica (APM), en el que las operaciones asincrónicas se
representan mediante un par de métodos Begin/End como FileStream.BeginRead y
Stream.EndRead.
Modelo asincrónico basado en eventos (EAP), en el que las operaciones asincrónicas se
representan mediante un par método-evento que se denomina OperationNameAsync y
OperationNameCompleted, por ejemplo, WebClient.DownloadStringAsync y
WebClient.DownloadStringCompleted. (EAP apareció por primera vez en .NET Framework
versión 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 asincrónicos. 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 código y aprovechar la siguiente
funcionalidad útil:
Registre las devoluciones de llamada, en el formulario de continuaciones de la tarea, en
cualquier momento después de que se haya iniciado la tarea.
Coordine varias operaciones que se ejecutan en respuesta a un método Begin_, mediante
los métodos ContinueWhenAny, ContinueWhenAll, WaitAll o WaitAny.
Encapsule las operaciones asincrónicas enlazadas a E/S y enlazadas a cálculos en el mismo
objeto de tarea.
Supervise el estado del objeto de tarea.
Calcule las referencias del estado una operación para un objeto de tarea mediante
TaskCompletionSource(Of TResult).
Ajustar las operaciones de APM en una tarea
Las clases System.Threading.Tasks.TaskFactory(Of TResult) y System.Threading.Tasks.TaskFactory
proporcionan varias sobrecargas de los métodos FromAsync que le permitieron encapsular un Begin
de APM/par de método de Fin en uno Task crean instancias o instancia Task(Of TResult). Las diversas
sobrecargas hospedan cualquier par de métodos de Begin/End que tenga entre cero y tres
parámetros de entrada.
Para los pares que tienen métodos End que devuelven un valor (Function en Visual Basic), use los
métodos de TaskFactory(Of TResult), que crean un objeto Task(Of TResult). Para los métodos End
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 49 de 107
que devuelven un valor void (Sub en Visual Basic), use los métodos de TaskFactory, que crean un
objeto Task.
En los pocos casos en los que el método Begin tiene más de tres parámetros o contiene parámetros
out o ref, se proporcionan las sobrecargas FromAsync adicionales que encapsulan sólo el método
End.
En el ejemplo de código siguiente se muestra la signatura para la sobrecarga FromAsync que
coincide con los métodos FileStream.BeginRead y FileStream.EndRead. Esta sobrecarga toma los tres
parámetros de entrada siguientes.
Public Function FromAsync(Of TArg1, TArg2, TArg3)(
ByVal beginMethod As Func(Of TArg1, TArg2, TArg3, AsyncCallback, Object, IAsyncResult),
ByVal endMethod As Func(Of IAsyncResult, TResult),
ByVal dataBuffer As TArg1,
ByVal byteOffsetToStartAt As TArg2,
ByVal maxBytesToRead As TArg3,
ByVal stateInfo As Object)
El primer parámetro es un delegado Func(Of T1, T2, T3, T4, T5, TResult) que coincide con la signatura
del método FileStream.BeginRead. El segundo parámetro es un delegado Func(Of 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(Of Int32). Los
últimos cuatro parámetros son idénticos a los del método FileStream.BeginRead:
Búfer donde se van a almacenar los datos de archivo.
El desplazamiento en el búfer en el que empezar a escribir los datos.
Cantidad máxima 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 devolución de llamada.
Usar ContinueWith para la funcionalidad de devolución de llamada
Si necesita obtener acceso a los datos del archivo, en contraposición a solo el número de bytes, el
método FromAsync no es suficiente. En su ligar, use Task(Of String), cuya propiedad Result contiene
los datos de archivo. Puede hacer si agrega una continuación a la tarea original. La continuación
realiza el trabajo que normalmente realizaría el delegado AsyncCallback. Se invoca cuando se
completa el antecedente y se ha rellenado el búfer de datos. (El objeto FileStream se debería cerrar
antes de devolver un valor).
En el siguiente ejemplo se muestra cómo devolver un objeto Task(Of String) que encapsula el par
BeginRead/EndRead de la clase FileStream.
Const MAX_FILE_SIZE As Integer = 14000000
Shared Function GetFileStringAsync(ByVal path As String) As Task(Of String)
Dim fi As New FileInfo(path)
Dim data(fi.Length) As Byte
Dim fs As FileStream = New FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, True)
' Task(Of Integer) returns the number of bytes read
Dim myTask As Task(Of Integer) = Task(Of Integer).Factory.FromAsync(
AddressOf fs.BeginRead, AddressOf fs.EndRead, data, 0, data.Length, Nothing)
' 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 myTask.ContinueWith(Function(antecedent)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 50 de 107
fs.Close()
If (antecedent.Result < 100) Then
Return "Data is too small to bother with."
End If
' If we did not receive the entire file, the end of the data buffer will contain garbage.
If (antecedent.Result < data.Length) Then
Array.Resize(data, antecedent.Result)
End If
' 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)
End Function)
End Function
A continuación, se puede llamar al método de la forma siguiente.
Dim myTask As Task(Of String) = GetFileStringAsync(path)
' Do some other work
' ...
Try
Console.WriteLine(myTask.Result.Substring(0, 500))
Catch ex As AggregateException
Console.WriteLine(ex.InnerException.Message)
End Try
Proporcionar los datos de estado personalizados
En las operaciones IAsyncResult típicas, si el delegado AsyncCallback requiere algún dato de estado
personalizado, tiene que pasarlo a través del último parámetro Begin para que los datos se puedan
empaquetar en el objeto IAsyncResult que se pasará finalmente al método de devolución de llamada.
Normalmente no se requiere esto cuando se usan los métodos FromAsync. Si los datos
personalizados son conocidos para la continuación, se pueden capturar directamente en el delegado
de continuación. El siguiente ejemplo se parece el ejemplo anterior, pero en lugar de examinar la
propiedad Result del antecedente, la continuación examina los datos de estado personalizados que
son directamente accesibles al delegado de usuario de la continuación.
Public Function GetFileStringAsync2(ByVal path As String) As Task(Of String)
Dim fi = New FileInfo(path)
Dim data(fi.Length) As Byte
Dim state As New MyCustomState()
Dim fs As 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.
Dim myTask As Task(Of Integer) = Task(Of Integer).Factory.FromAsync(
AddressOf fs.BeginRead, AddressOf fs.EndRead, data, 0, data.Length, Nothing)
Return myTask.ContinueWith(Function(antecedent)
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")) Then
Return "Start spreading the news!"
End If
' If we did not receive the entire file, the end of the data buffer will contain garbage.
If (antecedent.Result < data.Length) Then
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 51 de 107
Array.Resize(data, antecedent.Result)
End If
'/ 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)
End Function)
End Function
Sincronizar varias tareas FromAsync
Los métodos estáticos ContinueWhenAny y ContinueWhenAll proporcionan flexibilidad adicional
cuando se usan junto con los métodos FromAsync. El siguiente ejemplo muestra cómo iniciar varias
operaciones asincrónicas de E/S y, a continuación, espera a que todos ellas se completen antes de
ejecutar la continuación.
Public Function GetMultiFileData(ByVal filesToRead As String()) As Task(Of String)
Dim fs As FileStream
Dim tasks(filesToRead.Length) As Task(Of String)
Dim fileData() As Byte = Nothing
For i As Integer = 0 To filesToRead.Length
fileData(&H1000) = New Byte()
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(Of Integer).Factory.FromAsync(AddressOf fs.BeginRead, AddressOf fs.EndRead, fileData, 0,
fileData.Length, Nothing).
ContinueWith(Function(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) Then
ReDim Preserve fileData(antecedent.Result)
End If
'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)
End Function)
Next
Return Task(Of String).Factory.ContinueWhenAll(tasks, Function(data)
' Propagate all exceptions and mark all faulted tasks as observed.
Task.WaitAll(data)
' Combine the results from all tasks.
Dim sb As New StringBuilder()
For Each t As Task(Of String) In data
sb.Append(t.Result)
Next
' Final result to be returned eventually on the calling thread.
Return sb.ToString()
End Function)
End Function
Tareas FromAsync solo para el método End
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 52 de 107
En los pocos casos en los que el método Begin requiere más de tres parámetros de entrada o tiene
parámetros out o ref, puede usar las sobrecargas FromAsync, por ejemplo, TaskFactory(Of
TResult).FromAsync(IAsyncResult, Func(Of IAsyncResult, TResult)), que representa sólo el método
End. Estos métodos también se pueden usar en cualquier escenario en el que se pasa IAsyncResult y
desea encapsularlo en una tarea.
Shared Function ReturnTaskFromAsyncResult() As Task(Of String)
Dim ar As IAsyncResult = DoSomethingAsynchronously()
Dim t As Task(Of String) = Task(Of String).Factory.FromAsync(ar, Function(res) CStr(res.AsyncState))
Return t
End Function
Iniciar y cancelar las tareas FromAsync
La tarea devuelta por un método FromAsync tiene un estado de WaitingForActivation y la iniciará el
sistema en algún momento una vez creada la tarea. Si intenta llamar a Start en este tipo de tarea, se
producirá una excepción.
No puede cancelar una tarea FromAsync, porque las API subyacentes de .NET Framework admiten
actualmente la cancelación en curso del la E/S de archivo o red. Puede agregar la funcionalidad de
cancelación a un método que encapsula una llamada FromAsync, pero sólo puede responder a la
cancelación antes de que se llame a FromAsync o después de completar (por ejemplo, en una tarea
de continuación).
Algunas clases que admiten EAP, por ejemplo, WebClient, admiten la cancelación y esa funcionalidad
de cancelación nativa se puede integrar mediante los tokens de cancelación.
Exponer las operaciones de EAP complejas como tareas
La TPL no proporciona ningún método diseñado específicamente para encapsular una operación
asincrónica basada en eventos del mismo modo que la familia de métodos FromAsync ajusta el
modelo IAsyncResult. Sin embargo, TPL proporciona la clase
System.Threading.Tasks.TaskCompletionSource(Of TResult), que se puede usar para representar
cualquier conjunto arbitrario de operaciones como Task(Of TResult). Las operaciones pueden ser
sincrónicas o asincrónicas y pueden ser enlazadas a E/S o enlazadas a cálculo, o ambos.
En el siguiente ejemplo se muestra cómo usar TaskCompletionSource(Of TResult) para exponer un
conjunto de operaciones WebClient asincrónicas al código de cliente como un objeto Task básico. El
método permite escribir una matriz de direcciones URL de web y un término o nombre que se va a
buscar y, a continuación, devuelve el número de veces que aparece el término de búsqueda en cada
sitio.
Class SimpleWebExample
Dim tcs As New TaskCompletionSource(Of String())
Dim nameToSearch As String
Dim token As CancellationToken
Dim results As New List(Of String)
Dim m_lock As Object
Dim count As Integer
Dim addresses() As String
Public Function GetWordCountsSimplified(ByVal urls() As String, ByVal str As String, ByVal token As
CancellationToken) As Task(Of String())
Dim webClients() As WebClient
ReDim webClients(urls.Length)
' If the user cancels the CancellationToken, then we can use the
' WebClient's ability to cancel its own async operations.
token.Register(Sub()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 53 de 107
For Each wc As WebClient In webClients
If Not wc Is Nothing Then
wc.CancelAsync()
End If
Next
End Sub)
For i As Integer = 0 To urls.Length
webClients(i) = New WebClient()
' Specify the callback for the DownloadStringCompleted
' event that will be raised by this WebClient instance.
AddHandler webClients(i).DownloadStringCompleted, AddressOf WebEventHandler
Dim address As 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)
Next
' Return the underlying Task. The client code
' waits on the Result property, and handles exceptions
' in the try-catch block there.
Return tcs.Task
End Function
Public Sub WebEventHandler(ByVal sender As Object, ByVal args As DownloadStringCompletedEventArgs)
If args.Cancelled = True Then
tcs.TrySetCanceled()
Return
ElseIf Not args.Error Is Nothing Then
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.
Dim words() As String = args.Result.Split(" "c)
Dim NAME As String = nameToSearch.ToUpper()
Dim 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))
End If
SyncLock (m_lock)
count = count + 1
If (count = addresses.Length) Then
tcs.TrySetResult(results.ToArray())
End If
End SyncLock
End Sub
End Class
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 54 de 107
Recuerde que TaskCompletionSource iniciará cualquier tarea creada por TaskCompletionSource(Of
TResult) y, por consiguiente, el código de usuario no debería llamar al método 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 métodos Begin/End en una API. Por ejemplo, quizás 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 cómo usar las tareas para implementar un par de métodos
Begin/End de APM para un método enlazado a cálculo de ejecución prolongada.
Class Calculator
Public Function BeginCalculate(ByVal decimalPlaces As Integer, ByVal ac As AsyncCallback, ByVal state As Object)
As IAsyncResult
Console.WriteLine("Calling BeginCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId)
Dim myTask = Task(Of String).Factory.StartNew(Function(obj) Compute(decimalPlaces), state)
myTask.ContinueWith(Sub(antedecent) ac(myTask))
End Function
Private Function Compute(ByVal decimalPlaces As Integer)
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"
End Function
Public Function EndCalculate(ByVal ar As IAsyncResult) As String
Console.WriteLine("Calling EndCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId)
Return CType(ar, Task(Of String)).Result
End Function
End Class
Class CalculatorClient
Shared decimalPlaces As Integer
Shared Sub Main()
Dim calc As New Calculator
Dim places As Integer = 35
Dim callback As New AsyncCallback(AddressOf PrintResult)
Dim ar As IAsyncResult = calc.BeginCalculate(places, callback, calc)
' Do some work on this thread while the calulator is busy.
Console.WriteLine("Working...")
Thread.SpinWait(500000)
Console.ReadLine()
End Sub
Public Shared Sub PrintResult(ByVal result As IAsyncResult)
Dim c As Calculator = CType(result.AsyncState, Calculator)
Dim piString As String = c.EndCalculate(result)
Console.WriteLine("Calling PrintResult on thread {0}; result = {1}",
Thread.CurrentThread.ManagedThreadId, piString)
End Sub
End Class
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 55 de 107
Usar el código de ejemplo de StreamExtensions
El archivo Streamextensions.cs, en la página Samples for Parallel Programming with the .NET
Framework 4 del sitio web de MSDN, contiene varias implementaciones de la referencia que usan los
objetos de tarea para la E/S asincrónica de archivo y red.
1.3.2. Cómo: Encapsular modelos de EAP en una tarea
En el siguiente ejemplo se muestra cómo exponer las operaciones asincrónicas a una secuencia
arbitraria de Protocolo de autenticación extensible Host (EAP) como una tarea utilizando
TaskCompletionSource(Of TResult). El ejemplo también muestra cómo usar CancellationToken para
invocar los métodos de cancelación integrados en los objetos WebClient.
Ejemplo
Class WebDataDownLoader
Dim tcs As New TaskCompletionSource(Of String())
Dim nameToSearch As String
Dim token As CancellationToken
Dim results As New List(Of String)
Dim m_lock As Object
Dim count As Integer
Dim addresses() As String
Shared Sub Main()
Dim downloader As New WebDataDownLoader()
downloader.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"}
Dim cts As New CancellationTokenSource()
' Create a UI thread from which to cancel the entire operation
Task.Factory.StartNew(Sub()
Console.WriteLine("Press c to cancel")
If Console.ReadKey().KeyChar = "c"c Then
cts.Cancel()
End If
End Sub)
' Using a neutral search term that is sure to get some hits on English web sites.Please substitute your favorite search
term.
downloader.nameToSearch = "the"
Dim webTask As Task(Of String()) = downloader.GetWordCounts(downloader.addresses,
downloader.nameToSearch, cts.Token)
' Do some other work here unless the method has already completed.
If (webTask.IsCompleted = False) Then
' Simulate some work
Thread.SpinWait(5000000)
End If
Dim results As String() = Nothing
Try
results = webTask.Result
Catch ae As AggregateException
For Each ex As Exception In ae.InnerExceptions
If (TypeOf (ex) Is OperationCanceledException) Then
Dim oce As OperationCanceledException = CType(ex, OperationCanceledException)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 56 de 107
If oce.CancellationToken = cts.Token Then
Console.WriteLine("Operation canceled by user.")
End If
Else
Console.WriteLine(ex.Message)
End If
Next
End Try
If (Not results Is Nothing) Then
For Each item As String In results
Console.WriteLine(item)
Next
End If
Console.WriteLine("Press any key to exit")
Console.ReadKey()
End Sub
Public Function GetWordCounts(ByVal urls() As String, ByVal str As String, ByVal token As CancellationToken) As
Task(Of String())
Dim webClients() As WebClient
ReDim webClients(urls.Length)
m_lock = New Object()
' If the user cancels the CancellationToken, then we can use the WebClient's ability to cancel its own async
operations.
token.Register(Sub()
For Each wc As WebClient In webClients
If Not wc Is Nothing Then
wc.CancelAsync()
End If
Next
End Sub)
For i As Integer = 0 To urls.Length - 1
webClients(i) = New WebClient()
' Specify the callback for the DownloadStringCompleted event that will be raised by this WebClient instance.
AddHandler webClients(i).DownloadStringCompleted, AddressOf WebEventHandler
Dim address As Uri = Nothing
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 ex As UriFormatException
tcs.TrySetException(ex)
Return tcs.Task
End Try
Next
' Return the underlying Task. The client code waits on the Result property, and handles exceptions
' in the try-catch block there.
Return tcs.Task
End Function
Public Sub WebEventHandler(ByVal sender As Object, ByVal args As DownloadStringCompletedEventArgs)
If args.Cancelled = True Then
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 57 de 107
tcs.TrySetCanceled()
Return
ElseIf Not args.Error Is Nothing Then
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.
Dim words() As String = args.Result.Split(" "c)
Dim NAME As String = nameToSearch.ToUpper()
Dim 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, nameToSearch))
End If
SyncLock (m_lock)
count = count + 1
If (count = addresses.Length) Then
tcs.TrySetResult(results.ToArray())
End If
End SyncLock
End Sub

1.4. Problemas potenciales en el paralelismo de datos y tareas
En muchos casos, Parallel.For y Parallel.ForEach pueden proporcionar mejoras de rendimiento
significativas en comparación con los bucles secuenciales normales. Sin embargo, el trabajo de
paralelizar el bucle aporta una complejidad que puede llevar a problemas que, en código secuencial,
no son tan comunes o que no se producen en absoluto. En este tema se indican algunas prácticas
que se deben evitar al escribir bucles paralelos.
No se debe suponer que la ejecución en paralelo es siempre más rápida
En algunos casos, un bucle paralelo se podría ejecutar más lentamente que su equivalente secuencial.
La regla básica es que no es probable que los bucles en paralelo que tienen pocas iteraciones y
delegados de usuario rápidos 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 código secuencial, no es raro leer o escribir en variables estáticas o en campos de clase. Sin
embargo, cuando varios subprocesos tienen acceso a estas variables de forma simultánea, 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 sincronización 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(Of T) para almacenar el estado local del subproceso durante la
ejecución del bucle.
Evitar la paralelización excesiva
Si usa bucles en paralelo, incurrirá en costos de sobrecarga al crear particiones de la colección de
origen y sincronizar los subprocesos de trabajo. El número de procesadores del equipo reduce
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 58 de 107
también las ventajas de la paralelización. Si se ejecutan varios subprocesos enlazados a cálculos en
un único procesador, no se gana en velocidad. Por tanto, debe tener cuidado para no paralelizar en
exceso un bucle.
El escenario más común en el que se puede producir un exceso de paralelización son los bucles
anidados. En la mayoría de los casos, es mejor paralelizar únicamente el bucle exterior, a menos que
se cumplan una o más de las siguientes condiciones:
Se sabe que el bucle interno es muy largo.
Se realiza un cálculo caro en cada pedido. (La operación que se muestra en el ejemplo no es
cara.)
Se sabe que el sistema de destino tiene suficientes procesadores como para controlar el
número de subprocesos que se producirán 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 medición.
Evitar llamadas a métodos que no son seguros para subprocesos
La escritura en métodos de instancia que no son seguros para subprocesos de un bucle en paralelo
puede producir daños en los datos, que pueden pasar o no inadvertidos para el programa. También
puede dar lugar a excepciones. En el siguiente ejemplo, varios subprocesos estarían intentando
llamar simultáneamente al método FileStream.WriteByte, que no se admite en la clase.
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))
Limitar las llamadas a métodos seguros para subprocesos
La mayoría de los métodos estáticos de .NET Framework son seguros para subprocesos y se les
pueden llamar simultáneamente desde varios subprocesos. Sin embargo, incluso en estos casos, la
sincronización que esto supone puede conducir a una ralentización importante en la consulta.
Nota
Puede comprobarlo si inserta algunas llamadas a WriteLine en las consultas. Aunque este método se
utiliza en los ejemplos de la documentación para los efectos de demostración, no lo utilice en bucles
paralelos a menos que necesario.
Ser consciente de los problemas de afinidad de los subprocesos
Algunas tecnologías, 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 código 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.
Tener precaución 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. Esta optimización de rendimiento puede
acabar en interbloqueo en algunos casos. Por ejemplo, dos tareas podrían ejecutar el mismo código
de delegado, que señala cuándo se genera un evento, y después esperar a que la otra tarea señale. Si
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 59 de 107
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á señalar su evento. Para evitar que suceda, puede
especificar un tiempo de espera en la operación de espera o utilizar constructores de subproceso
explícitos 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(Of
TSource) tal vez no tengan que ejecutarse en paralelo. Por consiguiente, se debe evitar escribir
código cuya exactitud dependa de la ejecución en paralelo de las iteraciones o de la ejecución de las
iteraciones en algún orden concreto. Por ejemplo, es probable que este código lleve a un
interbloqueo:
Dim mres = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100) _
.AsParallel() _
.ForAll(Sub(j)
If j = Environment.ProcessorCount Then
Console.WriteLine("Set on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j)
mres.Set()
Else
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j)
mres.Wait()
End If
End Sub) ' deadlocks
En este ejemplo, una iteración 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
iteración 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 iteración del
valor de evento haya tenido oportunidad de ejecutarse. Esto produce un interbloqueo: la iteración
del valor de evento nunca se ejecutará y las iteraciones que esperan nunca se activarán.
En concreto, una iteración de un bucle paralelo no debe esperar nunca otra iteración del bucle para
progresar. Si el bucle paralelo decide programar las iteraciones secuencialmente pero en el orden
contrario, se producirá un interbloqueo.
Evitar la ejecución de bucles en paralelo en el subproceso de la interfaz de
usuario
Es importante mantener la interfaz de usuario de la aplicación (UI) capaz de reaccionar. Si una
operación contiene bastante trabajo para garantizar la paralelización, no se debería 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 después se
presentarán 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 cálculo básico se ha completado
se deberían calcular las referencias de la actualización 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
actualización 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 dañar el estado, a producir
excepciones, actualizaciones atrasadas e incluso interbloqueos, dependiendo de cómo se invoque la
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 60 de 107
actualización 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 iteración del bucle en un subproceso en segundo
plano (como puede hacer For), la llamada a Invoke produce que se envíe 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 Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim iterations As Integer = 20
Parallel.For(0, iterations, Sub(x)
Button1.Invoke(Sub()
DisplayProgress(x)
End Sub)
End Sub)
End Sub
En el siguiente ejemplo se muestra cómo evitar el interbloqueo mediante la ejecución 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 Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim iterations As Integer = 20
Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
Button1.Invoke(Sub()
DisplayProgress(x)
End Sub)
End Sub))
End Sub

2. Parallel LINQ (PLINQ)
Parallel LINQ (PLINQ) es una implementación paralela de LINQ to Objects. PLINQ implementa el
conjunto completo de operadores de consulta estándar de LINQ como métodos de extensión para el
espacio de nombres T:System.Linq y tiene operadores adicionales para las operaciones paralelas.
PLINQ combina la simplicidad y legibilidad de la sintaxis de LINQ con la eficacia de la programación
paralela. De la misma forma que el código destinado a la biblioteca TPL (Task Parallel Library,
biblioteca de procesamiento paralelo basado en tareas), las consultas PLINQ aumentan el grado de
simultaneidad en función de la capacidad del equipo host.
En muchos escenarios, PLINQ puede aumentar significativamente la velocidad de las consultas LINQ
to Objects utilizando todos los núcleos disponibles en el equipo host de una forma más eficaz. Este
mayor rendimiento aporta al escritorio una alta capacidad de computación.
2.1. Introducción a PLINQ
¿Qué es una consulta paralela?
Language-Integrated Query (LINQ) apareció por primera vez en .NET Framework 3.0. Representa un
modelo unificado para consultar cualquier origen de datos System.Collections.IEnumerable o
System.Collections.Generic. IEnumerable(Of T) con seguridad de tipos. LINQ to Objects es el nombre
para los consultas LINQ que se ejecutan con las colecciones en memoria, como List(Of T), y las
matrices. En este artículo se supone que tiene un conocimiento básico de LINQ.
Parallel LINQ (PLINQ) es una implementación paralela del modelo LINQ. Una consulta PLINQ se
parece de muchas maneras a una consulta LINQ to Objects no paralela. Las consultas PLINQ, al igual
que las consultas LINQ secuenciales, funcionan con cualquier origen de datos IEnumerable o
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 61 de 107
IEnumerable(Of T) en memoria y usan la ejecución diferida, lo que significa que no empiezan a
ejecutarse hasta que se enumere la consulta. La diferencia primaria es que PLINQ intenta realizar el
uso completo de todos los procesadores en el sistema. Para hacer esto, divide el origen de datos en
segmentos y, a continuación, ejecuta la consulta en cada segmento en subprocesos de trabajo
independientes en paralelo en varios procesadores. En muchos casos, la ejecución en paralelo
significa que la consulta se ejecuta bastante más rápidamente.
A través de la ejecución en paralelo, PLINQ puede lograr mejoras de rendimiento significativas
respecto al código heredado para ciertos tipos de consultas, a menudo con solo agregar la operación
de consulta AsParallel al origen de datos. Sin embargo, el paralelismo puede presentar sus propias
complejidades y no todas las operaciones de consulta se ejecutan más rápidamente en PLINQ. De
hecho, la ejecución en paralelo realmente reduce la velocidad de ciertas consultas. Por consiguiente,
debería entender en qué grado afectan a las consultas en paralelo ciertos aspectos como la
ordenación.
El resto de este artículo proporciona información general de las clases PLINQ principales y describe
cómo crear las consultas PLINQ. Cada sección contiene vínculos a ejemplos de código e información
más detallada.
La clase ParallelEnumerable
La clase System.Linq.ParallelEnumerable expone casi toda la funcionalidad de PLINQ. Esta clase y el
resto de tipos del espacio de nombres System.Linq se compilan en el ensamblado System.Core.dll.
Los proyectos predeterminados de C# y Visual Basic en Visual Studio hacen referencia el ensamblado
e importan el espacio de nombres.
ParallelEnumerable incluye implementaciones de todos los operadores de consulta estándar que
admite LINQ to Objects, aunque no intenta ejecutar cada una en paralelo.
Además de los operadores de consulta estándar, la clase ParallelEnumerable contiene un conjunto de
métodos que habilitan los comportamientos específicos de la ejecución en paralelo. Estos métodos
específicos de PLINQ se muestran en la siguiente tabla.
Operador
ParallelEnumerable
Descripción
AsParallel
Punto de entrada para PLINQ. Especifica que el resto de la consulta se
debería ejecutar en paralelo, si es posible.
AsSequential(Of TSource)
Especifica que el resto de la consulta se debería ejecutar
secuencialmente, como un consulta LINQ no paralela.
AsOrdered
Especifica que PLINQ debería conservar la clasificación de la secuencia
de origen para el resto de la consulta, o hasta que se cambie la
clasificación, por ejemplo mediante una cláusula orderby (Order By en
Visual Basic).
AsUnordered(Of TSource)
Especifica que no es necesario que PLINQ conserve la clasificación de
la secuencia de origen durante el resto de la consulta.
WithCancellation(Of TSource)
Especifica que PLINQ debería supervisar periódicamente el estado del
token de cancelación proporcionado y cancelar la ejecución si se
solicita.
WithDegreeOfParallelism(Of
TSource)
Especifica el número máximo de procesadores que PLINQ debería
usar para ejecutar la consulta en paralelo.
WithMergeOptions(Of
TSource)
Proporciona una sugerencia sobre cómo PLINQ debe, si es posible,
volver a combinar los resultados paralelos en solo una secuencia en el
subproceso utilizado.
WithExecutionMode(Of Especifica si PLINQ debería ejecutar la consulta en paralelo incluso
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 62 de 107
TSource) cuando el comportamiento predeterminado sería ejecutarla
secuencialmente.
ForAll(Of TSource)
Un método de enumeración multiproceso que, a diferencia de iterar
por los resultados de la consulta, permite volver a procesar los
resultados en paralelo sin volver a combinar primero el subproceso de
consumidor.
Sobrecarga Aggregate
Una sobrecarga que es exclusiva de PLINQ y habilita la agregación
intermedia sobre las particiones locales de subprocesos, más una
última función de agregación para combinar los resultados de todas
las particiones.
El modelo de opción
Al escribir una consulta, elija PLINQ invocando el método de extensión ParallelEnumerable.AsParallel
en el origen de datos, como se muestra en el siguiente ejemplo.
Dim source = Enumerable.Range(1, 10000)
' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
Where Compute(num) > 0
Select num
El método de extensión AsParallel enlaza a los operadores de consulta subsiguientes, en este caso,
where y select, a las implementaciones de System.Linq.ParallelEnumerable.
Modos de ejecución
De forma predeterminada, PLINQ es conservador. En tiempo de ejecución, la infraestructura PLINQ
analiza la estructura general de la consulta. Si es probable que la consulta produzca aumentos de
velocidad por la ejecución en paralelo, PLINQ divide la secuencia de origen en tareas que se pueden
ejecutar simultáneamente. Si no es seguro para ejecutar una consulta en paralelo, PLINQ
simplemente ejecuta la consulta de forma secuencial. Si PLINQ puede elegir entre un algoritmo
paralelo potencialmente costoso o un algoritmo secuencial poco costoso, elige el algoritmo
secuencial de forma predeterminada. Puede usar el método WithExecutionMode(Of TSource) y la
enumeración System.Linq.ParallelExecutionMode para indicar a PLINQ que seleccione el algoritmo
paralelo. Esto resulta útil cuando sabe por las pruebas y mediciones que una consulta determinada se
ejecuta más rápidamente en paralelo.
Grado de paralelismo
De forma predeterminada, PLINQ usa todos los procesadores en el equipo host hasta un máximo de
64. Puede indicar a PLINQ que no utilice más que un número especificado de procesadores usando el
método WithDegreeOf Parallelism(Of TSource). Esto resulta útil si desea asegurarse de que los otros
procesos que se ejecutan en el equipo reciben cierta cantidad de tiempo de CPU. El siguiente
fragmento de código limita la consulta a usar un máximo de dos procesadores.
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
Where Compute(item) > 42
Select item
En los casos donde una consulta está realizando una cantidad significativa de trabajo enlazado sin
cálculo, como la E/S de archivo, podría ser beneficioso especificar un grado de paralelismo mayor
que el número de núcleos del equipo.
Consultar en paralelo ordenadas frente a no ordenadas
En algunas consultas, un operador de consulta debe generar resultados que conservan el orden de la
secuencia de origen. PLINQ proporciona al operador AsOrdered para este propósito. AsOrdered es
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 63 de 107
distinto de AsSequential(Of TSource). Una secuencia AsOrdered todavía se procesa en paralelo, pero
sus resultados están almacenados en búfer y ordenan. Dado que la preservación del orden implica el
trabajo extraordinario normalmente, se podría procesar una secuencia AsOrdered más despacio que
la secuencia AsUnordered(Of TSource) predeterminada. El hecho de que una operación en paralelo
ordenada especial sea más rápida que una versión secuencial de la operación depende de muchos
factores.
El siguiente ejemplo de código muestra las opciones que se deben elegir para conservar el orden.
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
Where num Mod 2 = 0
Select num
Consultas enparalelo frente a secuenciales
Algunas operaciones requieren que los datos de origen se entreguen de una manera secuencial. Los
operadores de consulta ParallelEnumerable revierten automáticamente al modo secuencial cuando es
necesario. Para los operadores de consulta definidos por el usuario y los delegados de usuario que
requieren la ejecución secuencial, PLINQ proporciona el método AsSequential(Of TSource). Al usar
AsSequential(Of TSource), se ejecutan secuencialmente todos los operadores subsiguientes en la
consulta hasta que se llame de nuevo a AsParallel.
Opciones para combinar los resultados de la consulta
Cuando una consulta PLINQ se ejecuta en paralelo, sus resultados de cada subproceso de trabajo
deben volver a combinarse en el subproceso principal para su uso en un bucle foreach (For Each en
Visual Basic) o la inserción en una lista o matriz. En algunos casos, podría ser beneficioso especificar
un tipo determinado de operación de combinación, por ejemplo, para empezar a generar resultados
más rápidamente. Para este propósito, PLINQ admite el método WithMergeOptions(Of TSource)y la
enumeración ParallelMergeOptions.
El operador ForAll
En consultas LINQ secuenciales, la ejecución se difiere hasta que la consulta se enumere o en un
bucle foreach (For Each en Visual Basic) o invocando un método como ToList(Of TSource), ToArray(Of
TSource)o ToDictionary. En PLINQ, también se puede usar foreach para ejecutar la consulta e iterar
por los resultados. Sin embargo, el propio bucle foreach no se ejecuta en paralelo y, por
consiguiente, requiere que el resultado de todas las tareas paralelas se vuelva a combinar en el
subproceso en el que se está ejecutando el bucle. En PLINQ, puede usar foreach cuando deba
conservar el orden final de los resultados de la consulta, y también cada vez que procese los
resultados en serie, por ejemplo cuando está llamando a Console.WriteLine para cada elemento. Para
una ejecución más rápida de la consulta cuando no se requiere la conservación del orden y cuando el
propio procesamiento de los resultados se puede ejecutar en paralelo, use el método ForAll(Of
TSource) para ejecutar una consulta PLINQ. ForAll(Of TSource) no realiza este paso final de la
combinación. En el siguiente ejemplo de código, se muestra cómo utilizar el método ForAll(Of
TSource). Se usa System.Collections.Concurrent.ConcurrentBag(Of T) en este caso porque está
optimizado para varios subprocesos que agregan elementos simultáneamente sin intentar quitar
ningún elemento.
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
Where num Mod 10 = 0
Select num
' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 64 de 107
En la siguiente ilustración, se muestra la diferencia entre foreach y ForAll(Of TSource) con respecto a
la ejecución de una consulta.

Cancelación
PLINQ se integra con los tipos de cancelación en .NET Framework 4. (Para obtener más información,
vea Cancelación). Por consiguiente, a diferencia de las consultas secuenciales LINQ to Objects, las
consultas PLINQ pueden cancelarse. Crear una consulta PLINQ cancelable, use el operador
WithCancellation(Of TSource) en la consulta y proporcione una instancia de CancellationToken como
argumento. Cuando se establece en true la propiedad IsCancellationRequested del token, PLINQ lo
observará, detendrá el procesamiento en todos los subprocesos y generará una excepción
OperationCanceledException.
Es posible que una consulta PLINQ continúe procesando algunos elementos una vez establecido el
token de cancelación.
Para lograr una sensibilidad mayor, puede responder también a las solicitudes de cancelación en los
delegados de usuario de ejecución prolongada.
Excepciones
Cuando se ejecuta una consulta PLINQ, se podrían producir simultáneamente varias excepciones de
diferentes subprocesos. Asimismo, el código para controlar la excepción podría encontrarse un
subproceso diferente al del código que produjo la excepción. PLINQ usa el tipo AggregateException
para encapsular todas las excepciones producidas por una consulta y vuelve a calcular las referencias
de esas excepciones para el subproceso que realiza la llamada. En el subproceso que realiza la
llamada, solo se requiere un bloque try-catch. Sin embargo, puede iterar por todas las excepciones
encapsuladas en AggregateException y detectar cualquiera de la que se pueda recuperar de forma
segura. En casos raros, algunas excepciones se puede producir que no se ajusta en un
AggregateExceptiony tampoco se ajusta ThreadAbortExceptions.
Cuando las excepciones pueden propagarse de nuevo al subproceso de unión, es posible que una
consulta continúe procesando algunos elementos después de que se haya producido la excepción.
Particionadores personalizados
En ciertos casos, puede mejorar el rendimiento de las consultas si escribe un particionador
personalizado que se aprovecha de alguna característica de los datos de origen. En la consulta, el
propio particionador personalizado es el objeto enumerable que se consulta.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 65 de 107
Dim arr(10000) As Integer
Dim partitioner = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
PLINQ admite un número de particiones fijo (aunque los datos se pueden reasignar dinámicamente a
esas particiones durante el tiempo de ejecución para el equilibrio de carga). For y ForEach solo
admiten la creación dinámica de particiones, lo que significa que el número de particiones cambia en
tiempo de ejecución.
Medir el rendimiento de PLINQ
En muchos casos, una consulta se puede ejecutar en paralelo, pero la sobrecarga de preparar la
consulta paralela supera a la ventaja de rendimiento obtenida. Si una consulta no realiza muchos
cálculos o si el origen de datos es pequeño, una consulta PLINQ puede ser más lenta que una
consulta secuencial LINQ to Objects. Puede usar el Analizador de rendimiento de procesamiento
paralelo en Visual Studio Team Server para comparar el rendimiento de varias consultas, buscar los
cuellos de botella del procesamiento y determinar si su consulta se está ejecutando en paralelo o
secuencialmente.
2.2. Introducción a la velocidad en PLINQ
El objetivo principal de PLINQ es acelerar la ejecución de LINQ to Objects mediante la ejecución de
los delegados de consulta en paralelo en equipos multiprocesador. El rendimiento de PLINQ es
óptimo cuando el procesamiento de cada elemento de una colección de origen es independiente, y
no se comparte el estado entre los delegados individuales. Tales operaciones son comunes en LINQ a
Objects y PLINQ y se llaman a menudo "encantadamente paralelo" porque se prestan con facilidad a
la programación en varios subprocesos. Sin embargo, no todas las consultas se componen de
operaciones paralelas perfectas; en la mayoría de los casos, una consulta incluye operadores que no
se pueden paralelizar o que ralentizan la ejecución en paralelo. Incluso con consultas que son
perfectamente paralelas, PLINQ debe crear particiones del origen de datos y programar el trabajo en
los subprocesos, y generalmente tiene que combinar los resultados cuando la consulta se completa.
Todas estas operaciones aumentan el costo computacional de la paralelización; el costo de agregar
paralelización se denomina sobrecarga. Para lograr el rendimiento óptimo en una consulta PLINQ, el
objetivo es maximizar las partes que son perfectamente paralelas y minimizar las que requieren
sobrecarga. En este artículo se proporciona información que le ayudará a escribir consultas PLINQ lo
más eficaces posible y que además produzcan resultados correctos.
Factores que afectan al rendimiento de las consultas PLINQ
En las siguientes secciones se enumeran algunos de los factores más importantes que influyen en el
rendimiento de las consultas en paralelo. Son instrucciones generales que por sí solas no bastan para
predecir el rendimiento de las consultas en todos los casos. Como siempre, es importante medir el
rendimiento real de consultas concretas en equipos con una gama de cargas y configuraciones
representativas.
1. Costo computacional del trabajo total.
Para lograr velocidad, una consulta PLINQ debe tener bastante trabajo perfectamente en
paralelo como para compensar la sobrecarga. El trabajo se puede expresar como el costo
computacional de cada delegado multiplicado por el número de elementos de la colección
de origen. Suponiendo que una operación se pueda paralelizar, cuanto más cara sea
computacionalmente, más oportunidad hay de aumentar la velocidad. Por ejemplo, si una
función tarda un milisegundo en ejecutarse, una consulta secuencial de más de 1000
elementos tardará un segundo en realizar esa operación, mientras que una consulta paralela
en un equipo con cuatro núcleos solo tardaría 250 milisegundos. Esto supone 750
milisegundos menos. Si la función tardara un segundo en ejecutar cada elemento, el
aumento sería de 750 segundos. Si el delegado resulta muy caro, PLINQ podría proporcionar
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 66 de 107
un aumento significativo con solo unos elementos de la colección de origen. A la inversa, las
colecciones de origen pequeñas con delegados triviales no son, en general, buenas
candidatas para PLINQ.
En el siguiente ejemplo, queryA es probablemente buena candidata para PLINQ, suponiendo
que su función Select implica mucho trabajo. queryB no es probablemente una buena
candidata porque no hay bastante trabajo en la instrucción Select y la sobrecarga de
paralelización compensará la mayoría del aumento (o todo).
Dim queryA = From num In numberList.AsParallel()
Select ExpensiveFunction(num); 'good for PLINQ
Dim queryB = From num In numberList.AsParallel()
Where num Mod 2 > 0
Select num; 'not as good for PLINQ
Este idioma no es compatible o no hay ningún ejemplo de código disponible.
2. Número de núcleos lógicos del sistema (grado de paralelismo).
Este punto es un corolario obvio de la sección anterior. Las consultas que están
perfectamente en paralelo se ejecutan más rápidamente en equipos con más núcleos
porque se puede dividir el trabajo entre más subprocesos simultáneos. La cantidad total de
aumento de velocidad depende del porcentaje del trabajo total de la consulta que se puede
paralelizar. Sin embargo, no se debe dar por supuesto que todas las consultas se ejecutarán
el doble de rápido en un equipo de ocho núcleos que en uno de cuatro. Al refinar las
consultas para lograr el rendimiento óptimo, es importante medir los resultados reales en
equipos con varios núcleos. Este punto se relaciona con el punto 1: se necesitan conjuntos
de datos mayores para aprovechar los grandes recursos informáticos.
3. Número y tipo de operaciones.
PLINQ proporciona el operador AsOrdered para las situaciones en las que es necesario
mantener el orden de elementos de la secuencia de origen. Hay un costo asociado a la
ordenación, pero suele ser reducido. Las operaciones GroupBy y Join también incurren en
sobrecarga. PLINQ rinde mejor cuando puede procesar los elementos de la colección de
origen en cualquier orden y pasarlos al operador siguiente en cuanto están listos.
4. Forma de ejecución de la consulta.
Si se guardan los resultados de una consulta llamando a ToArray o ToList, los resultados de
todos los subprocesos paralelos deben combinarse en la estructura de datos única. Esto
implica un costo computacional inevitable. De igual modo, si se recorren los resultados con
un bucle foreach (For Each en Visual Basic), hay que serializar los resultados de los
subprocesos de trabajo en el subproceso del enumerador. Pero si solo desea realizar una
acción basada en el resultado de cada subproceso, puede utilizar el método ForAll para
realizar este trabajo en varios subprocesos.
5. Tipo de opciones de combinación.
PLINQ se puede configurar para almacenar en búfer el resultado y producirlo en fragmentos
o todo a la vez cuando el conjunto de resultados esté completo, o transmitir en secuencias
los resultados individuales a medida que se van produciendo. El primer resultado disminuye
el tiempo de ejecución total y el segundo disminuye la latencia entre los elementos
producidos. Aunque las opciones de combinación no siempre tienen un efecto importante
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 67 de 107
en el rendimiento global de las consultas, pueden influir en el rendimiento percibido, ya que
controlan cuánto tiempo debe esperar un usuario para ver los resultados.
6. Tipo de creación de particiones.
En algunos casos, una consulta PLINQ sobre una colección de origen indizable puede
producir una carga de trabajo desequilibrada. Cuando suceda, tal vez logre aumentar el
rendimiento de las consultas creando un particionador personalizado.
Cuándo elige PLINQ el modo secuencial
PLINQ siempre intentará ejecutar una consulta con la misma rapidez que si la consulta se ejecutara
secuencialmente. Aunque PLINQ no tenga en cuenta en el costo computacional de los delegados de
usuario ni el tamaño del origen de entrada, sí busca ciertas "formas" de consulta. Específicamente,
busca operadores de consulta o combinaciones de operadores que hacen que normalmente una
consulta se ejecute más despacio en modo paralelo. Cuando encuentra esas formas, PLINQ vuelve de
forma predeterminada al modo secuencial.
Sin embargo, después de medir el rendimiento de una consulta concreta, puede determinar que
realmente se ejecuta más rápidamente en modo paralelo. En casos así puede utilizar la marca
ParallelExecutionMode.ForceParallelism a través del método ParallelEnumerableWithExecutionMode
para indicar a PLINQ que paralelice la consulta.
La siguiente lista describe las formas de consulta que PLINQ ejecutará de forma predeterminada en
modo secuencial:
Consultas que contienen Select, Where indizado, SelectMany indizado o una cláusula
ElementAt después de un operador de clasificación o de filtrado que ha quitado o
reorganizado los índices originales.
Consultas que contienen un operador Take, TakeWhile, Skip, SkipWhile y donde los índices
de la secuencia de origen no están en el orden original.
Consultas que contienen Zip o SequenceEquals, a menos que uno de los orígenes de datos
tenga un índice ordenado inicialmente y el otro origen de datos sea indizable (es decir una
matriz o IList (T)).
Consultas que contienen Concat, a menos que se apliquen a orígenes de datos indizables.
Consultas que contienen Reverse, a menos que se apliquen a un origen de datos indizable.
2.3. Conservar el orden en PLINQ
En PLINQ, el objetivo es maximizar el rendimiento manteniendo la exactitud. Una consulta se debería
ejecutar lo más rápido que fuese posible pero con resultados correctos. La exactitud exige que se
conserve el orden de la secuencia de origen en algunos casos; sin embargo, la ordenación puede
suponer la utilización de muchos recursos de computación. Por consiguiente, de forma
predeterminada, PLINQ no conserva el orden de la secuencia de origen. En este sentido, PLINQ se
parece a LINQ to SQL, pero es diferente de LINQ to Objects, que conserva el orden.
Para reemplazar el comportamiento predeterminado, puede activar la capacidad de conservar el
orden utilizando el operador AsOrdered en la secuencia de origen. Después, puede desactivarla en la
consulta, utilizando el método AsUnordered(Of TSource). Con ambos métodos, la consulta se procesa
basado en la heurística que determina si ejecutar la consulta como paralelo o como secuencial.
En el siguiente ejemplo se muestra una consulta paralela no ordenada que filtra todos los elementos
que coinciden con una condición, sin intentar ordenar los resultados de forma alguna.
Dim cityQuery = From city In cities.AsParallel() Where City.Population > 10000 Take (1000)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 68 de 107
Esta consulta no obtiene necesariamente las 1000 primeras ciudades de la secuencia de origen que
cumplen la condición, sino que algún conjunto de las 1000 ciudades que la cumplen. Los operadores
de consulta PLINQ particionan la secuencia de origen en varias secuencias secundarias que se
procesan como tareas simultáneas. Si no se especifica que se conserve el orden, los resultados de
cada partición se presentan a la siguiente etapa de la consulta con un orden arbitrario. Por otra parte,
una partición puede producir un subconjunto de los resultados antes de continuar procesando los
elementos restantes. El orden resultante puede ser diferente cada vez. Una aplicación no puede
controlar este hecho, porque depende de cómo programe los subprocesos el sistema operativo.
En el siguiente ejemplo se reemplaza el comportamiento predeterminado utilizando al operador
AsOrdered en la secuencia de origen. De esta forma se garantiza que el método Take(Of TSource)
devuelve las 10 primeras ciudades de la secuencia de origen que cumplen la condición.
Dim orderedCities = From city In cities.AsParallel().AsOrdered() Where City.Population > 10000 Take (1000)
Sin embargo, esta consulta probablemente no se ejecute tan rápido como la versión no ordenada,
porque debe realizar el seguimiento del orden original en todas las particiones y, en el momento de
la combinación, garantizar que el orden es coherente. Por consiguiente, recomendamos usar
AsOrdered solo cuando sea estrictamente necesario y únicamente para las partes de la consulta que
lo requieran. Cuando ya no sea necesario conservar el orden, use AsUnordered(Of TSource) para
desactivarlo. En el siguiente ejemplo se consigue mediante la creación de dos consultas.
Dim orderedCities2 = From city In cities.AsParallel().AsOrdered()
Where city.Population > 10000
Select city
Take (1000)
Dim finalResult = From city In orderedCities2.AsUnordered()
Join p In people.AsParallel() On city.Name Equals p.CityName
Select New With {.Name = city.Name, .Pop = city.Population, .Mayor = city.Mayor}
For Each city In finalResult
Console.WriteLine(city.Name & ":" & city.Pop & ":" & city.Mayor)
Next
Observe que PLINQ conserva el orden de una secuencia generada por operadores que imponen el
orden para el resto de la consulta. En otras palabras, los operadores de tipo OrderBy y ThenBy se
tratan como si fuesen seguidos de una llamada a AsOrdered.
Operadores de consulta y ordenación
Los siguientes operadores de consulta introducen la conservación del orden en todas las operaciones
posteriores de una consulta o hasta que se llame a AsUnordered(Of TSource):
OrderBy
OrderByDescending
ThenBy
ThenByDescending
En algunos casos, los siguientes operadores de consulta PLINQ pueden requerir secuencias de origen
ordenadas para generar resultados correctos:
Reverse(Of TSource)
SequenceEqual
TakeWhile
SkipWhile
Zip
Algunos operadores de consulta PLINQ se comportan de manera diferente, dependiendo de si su
secuencia de origen está ordenada o no. En la siguiente tabla se enumeran estos operadores.
operador ??
Resultado cuando la secuencia de
origen está ordenada
Resultado cuando la secuencia de
origen no está ordenada
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 69 de 107
Aggregate
Salida no determinista para
operaciones no asociativas o no
conmutativas.
Salida no determinista para
operaciones no asociativas o no
conmutativas.
All(Of TSource) No es aplicable No es aplicable
Any No es aplicable No es aplicable
AsEnumerable(Of
TSource)
No es aplicable No es aplicable
Average
Salida no determinista para
operaciones no asociativas o no
conmutativas.
Salida no determinista para
operaciones no asociativas o no
conmutativas.
Cast(Of TResult) Resultados ordenados Resultados no ordenados
Concat Resultados ordenados Resultados no ordenados
Count No es aplicable No es aplicable
DefaultIfEmpty No es aplicable No es aplicable
Distinct Resultados ordenados Resultados no ordenados
ElementAt(Of TSource) Se devuelve el elemento especificado Elemento arbitrario
ElementAtOrDefault(Of
Tsource)
Se devuelve el elemento especificado Elemento arbitrario
Except Resultados no ordenados Resultados no ordenados
First Se devuelve el elemento especificado Elemento arbitrario
FirstOrDefault Se devuelve el elemento especificado Elemento arbitrario
ForAll(Of TSource) Ejecución no determinista en paralelo
Ejecución no determinista en
paralelo
GroupBy Resultados ordenados Resultados no ordenados
GroupJoin Resultados ordenados Resultados no ordenados
Intersect Resultados ordenados Resultados no ordenados
Join Resultados ordenados Resultados no ordenados
Last Se devuelve el elemento especificado Elemento arbitrario
LastOrDefault Se devuelve el elemento especificado Elemento arbitrario
LongCount No es aplicable No es aplicable
Min No es aplicable No es aplicable
OrderBy Reordena la secuencia Inicia una nueva sección ordenada
OrderByDescending Reordena la secuencia Inicia una nueva sección ordenada
Range
No aplicable (mismo valor
predeterminado como AsParallel )
No es aplicable
Repeat(Of TResult)
No aplicable (mismo valor
predeterminado que AsParallel)
No es aplicable
Reverse(Of TSource) Invierte el orden No hace nada
Select Resultados ordenados Resultados no ordenados
Select (se indiza) Resultados ordenados Resultados no ordenados.
SelectMany Resultados ordenados. Resultados no ordenados
SelectMany (se indiza) Resultados ordenados. Resultados no ordenados.
SequenceEqual Comparación ordenada Comparación no ordenada
Single No es aplicable No es aplicable
SingleOrDefault No es aplicable No es aplicable
Skip(Of TSource) Omite los n primeros elementos Omite cualquier elemento n
SkipWhile Resultados ordenados.
No determinista. Ejecuta SkipWhile
en el orden arbitrario actual
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 70 de 107
Sum
Salida no determinista para
operaciones no asociativas o no
conmutativas.
Salida no determinista para
operaciones no asociativas o no
conmutativas.
Take(Of TSource) Toma los n primeros elementos Toma cualquier elemento n
TakeWhile Resultados ordenados
No determinista. Ejecuta TakeWhile
en el orden arbitrario actual
ThenBy Complementa OrderBy Complementa OrderBy
ThenByDescending Complementa OrderBy Complementa OrderBy
ToArray(Of TSource) Resultados ordenados Resultados no ordenados
ToDictionary No es aplicable No es aplicable
ToList(Of TSource) Resultados ordenados Resultados no ordenados
ToLookup Resultados ordenados Resultados no ordenados
Union Resultados ordenados Resultados no ordenados
Where Resultados ordenados Resultados no ordenados
Where (se indiza) Resultados ordenados Resultados no ordenados
Zip Resultados ordenados Resultados no ordenados
Activamente no se barajan los resultados no ordenados; simplemente no tienen ninguna lógica de la
clasificación especial aplicada a ellos. En algunos casos, una consulta no ordenada puede retener la
clasificación de la secuencia del origen. Para las consultas que utilizan al operador Select el indizado,
garantías PLINQ que los elementos de salida entrarán fuera en el orden de aumentar los índices, pero
no realiza ninguna garantía sobre la que los índices tendrán asignado a que los elementos.

2.4. Opciones de combinación en PLINQ
Cuando una consulta se ejecuta en paralelo, PLINQ crea particiones en la secuencia de origen para
que varios subprocesos puedan funcionar simultáneamente en diferentes partes, por lo general en
subprocesos independientes. Si los resultados se van a usar en un subproceso, por ejemplo, en un
bucle foreach (For Each en Visual Basic), los resultados de cada subproceso deben volver a
combinarse en una secuencia. El tipo de combinación que PLINQ realiza depende de los operadores
que se encuentran en la consulta. Por ejemplo, los operadores que imponen un nuevo orden en los
resultados deben almacenar en búfer todos los elementos de todos los subprocesos. Desde el punto
de vista del subproceso utilizado (qué también es el del usuario de la aplicación), una consulta
totalmente almacenada en búfer podría ejecutarse durante un período notable de tiempo antes de
generar su primer resultado. Otros operadores se almacenan parcialmente en búfer de forma
predeterminada; producen sus resultados en lotes. Un operador, ForAll(Of TSource)no está
almacenado en búfer de forma predeterminada. Produce inmediatamente todos los elementos de
todos los subprocesos.
Como se muestra en el siguiente ejemplo, utilizando el método WithMergeOptions(Of TSource),
puede proporcionar una sugerencia a PLINQ que indica qué tipo de combinar para realizar.
Dim scanlines = From n In nums.AsParallel().WithMergeOptions(ParallelMergeOptions.NotBuffered)
Where n Mod 2 = 0
Select ExpensiveFunc(n)
Si la consulta determinada no puede admitir la opción solicitada, esta se omitirá. En la mayoría de los
casos, no tiene que especificar una opción de combinación para una consulta PLINQ. Sin embargo,
en algunos casos puede encontrar mediante pruebas y mediciones que una consulta se ejecuta mejor
en un modo no predeterminado. Un uso común de esta opción es forzar a un operador de
combinación de fragmentos a transmitir en secuencias sus resultados para proporcionar una interfaz
de usuario más sensible.
ParallelMergeOptions
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 71 de 107
La enumeración ParallelMergeOptions incluye las siguientes opciones que especifican, para las
formas de la consulta compatibles, cómo se proporciona el resultado final de la consulta cuando se
usan los resultados en un subproceso:
Not Buffered
La opción NotBuffered produce cada elemento procesado que se va a devolver de cada
subproceso en cuanto se genere. Este comportamiento es análogo a la "transmisión por
secuencias" del resultado. Si el operador AsOrdered se encuentra en la consulta, NotBuffered
conserva el orden de los elementos de origen. Aunque NotBuffered empieza a proporcionar
los resultados en cuanto están disponibles, el tiempo total para generar todos los resultados
podría ser aún más prolongado que al usar una de las otras opciones de combinación.
Auto Buffered
La opción AutoBuffered hace que la consulta recoja los elementos en un búfer y a
continuación periódicamente produce de repente el contenido del búfer al subproceso para
utilizar. Esto es análogo a proporcionar los datos de origen en "fragmentos" en lugar de usar
el comportamiento de "transmisión por secuencias" de NotBuffered. AutoBuffered puede
llevar mucho más tiempo que NotBuffered para hacer que el primer elemento esté
disponible en el subproceso utilizado. El tamaño del búfer y el comportamiento productivo
exacto no se puede configurar y puede variar, en función de varios factores relacionados con
la consulta.
FullyBuffered
La opción FullyBuffered produce el resultado de la consulta entera que se va a estar
almacenado en búfer antes de cualquiera de los elementos se produce. Cuando se usa esta
opción, puede llevar mucho más tiempo que el primer elemento esté disponible en el
subproceso utilizado, pero los resultados completos se podrían generar aún más
rápidamente que al usar las otras opciones.
Operadores de consulta que admiten opciones de combinación
En la siguiente tabla se hace una lista de los operadores que admiten todos los modos de opción de
combinación, sujetos a las restricciones especificadas.
operador ?? Restricciones
AsEnumerable(Of TSource) Ninguno
Cast(Of TResult) Ninguno
Concat Consultas no ordenadas que solo tienen un origen de matriz o lista.
DefaultIfEmpty Ninguno
OfType(Of TResult) Ninguno
Reverse(Of TSource) Consultas no ordenadas que solo tienen un origen de matriz o lista.
Select Ninguno
SelectMany Ninguno
Skip(Of TSource) Ninguno
Take(Of TSource) Ninguno
Where Ninguno
Todos los demás operadores de consulta PLINQ podrían omitir las opciones de combinación
proporcionadas por el usuario. Algunos operadores de consulta, por ejemplo, Reverse(Of TSource) y
OrderBy, no pueden proporcionar ningún elemento hasta que todos se hayan generado y
reordenado. Por consiguiente, cuando se usa ParallelMergeOptions en una consulta que también
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 72 de 107
contiene a operador como Reverse(Of TSource), el comportamiento de combinación no se aplicará
en la consulta hasta que ese operador haya generado sus resultados.
La capacidad de algunos operadores de administrar las opciones de combinación depende del tipo
de la secuencia del origen y si el operador AsOrdered se utilizó anteriormente en la consulta.
ForAll(Of TSource) siempre es NotBuffered; produce sus elementos inmediatamente. OrderBy
siempre es FullyBuffered; debe ordenar la lista entera antes de producir.
2.5. Posibles problemas con PLINQ
En muchos casos, PLINQ puede proporcionar importantes mejoras de rendimiento con respecto a las
consultas LINQ to Objects. Sin embargo, el trabajo de paralelizar la ejecución de las consultas aporta
una complejidad que puede conducir a problemas que, en código secuencial, no son tan comunes o
que no se producen en absoluto. En este tema se indican algunas prácticas que se deben evitar al
escribir consultas PLINQ.
No se debe suponer que la ejecución en paralelo es siempre más rápida
En ocasiones, la paralelización hace que una consulta PLINQ se ejecute con mayor lentitud que su
equivalente LINQ to Objects. La regla básica es que no es probable que las consultas con pocos
elementos de origen y delegados de usuario rápidos vayan mucho más rápido. Sin embargo, dado
que hay muchos factores que afectan al rendimiento, se recomienda medir los resultados reales antes
de decidir si se usa PLINQ.
Evitar la escritura en ubicaciones de memoria compartidas
En código secuencial, no es raro leer o escribir en variables estáticas o en campos de clase. Sin
embargo, cuando varios subprocesos tienen acceso a estas variables de forma simultánea, 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 sincronización puede afectar negativamente al
rendimiento. Por tanto, se recomienda evitar, o al menos limitar, el acceso al estado compartido en
una consulta PLINQ en la medida de lo posible.
Evitar la paralelización excesiva
Si usa el operador AsParallel, incurre en costos de sobrecarga al crear particiones de la colección de
origen y sincronizar los subprocesos de trabajo. El número de procesadores del equipo reduce
también las ventajas de la paralelización. Si se ejecutan varios subprocesos enlazados a cálculos en
un único procesador, no se gana en velocidad. Por tanto, debe tener cuidado para no paralelizar en
exceso una consulta.
El escenario más común en el que se puede producir un exceso de paralelización son las consultas
anidadas, tal como se muestra en el siguiente fragmento de código.
Dim q = From cust In customers.AsParallel()
From order In cust.Orders.AsParallel()
Where order.OrderDate > aDate
Select New With {cust, order}
En este caso, es mejor paralelizar únicamente el origen de datos exterior (clientes) a menos que se
cumplan una o más de las siguientes condiciones:
Se sabe que el origen de datos interno (cust.Orders) es muy largo.
Se realiza un cálculo caro en cada pedido. (La operación que se muestra en el ejemplo no es
cara.)
Se sabe que el sistema de destino tiene suficientes procesadores como para controlar el
número de subprocesos que se producirán al paralelizar la consulta de cust.Orders.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 73 de 107
En todos los casos, la mejor manera de determinar la forma óptima de la consulta es mediante la
prueba y la medición.
Evitar llamadas a métodos que no son seguros para subprocesos
La escritura en métodos de instancia que no son seguros para subprocesos de una consulta PLINQ
puede producir daños en los datos, que pueden pasar o no desapercibidos en el programa. También
puede dar lugar a excepciones. En el siguiente ejemplo, varios subprocesos estarían intentando
llamar simultáneamente al método Filestream.Write, que no se admite en la clase.
Dim fs As FileStream = File.OpenWrite(…) a.Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
Limitar las llamadas a métodos seguros para subprocesos
La mayoría de los métodos estáticos de .NET Framework son seguros para subprocesos y se les
pueden llamar simultáneamente desde varios subprocesos. Sin embargo, incluso en estos casos, la
sincronización que esto supone puede conducir a una ralentización importante en la consulta.
Evitar operaciones de ordenación innecesarias
Cuando PLINQ ejecuta una consulta en paralelo, divide la secuencia de origen en particiones con las
que se puede trabajar de forma simultánea en varios subprocesos. De forma predeterminada, el
orden en que se procesan las particiones y se entregan los resultados no es predecible (excepto para
operadores como OrderBy). Puede indicar a PLINQ que conserve la ordenación de las secuencias de
origen, pero esto tiene un impacto negativo en el rendimiento. El procedimiento recomendado,
siempre que sea posible, consiste en estructurar las consultas de forma que no dependan del
mantenimiento del orden.
Preferir ForAll a ForEach cuando sea posible
Si bien PLINQ ejecuta una consulta en varios subprocesos, si utiliza los resultados en un bucle foreach
(For Each en Visual Basic), los resultados de la consulta se deben combinar de nuevo en un único
subproceso y el enumerador debe tener acceso a ellos en serie. En algunos casos, esto es inevitable;
sin embargo, siempre que sea posible, utilice el método ForAll para permitir que cada subproceso
genera sus propios resultados, por ejemplo, escribiendo en una colección segura para subprocesos
como ConcurrentBag.
El mismo problema se aplica a ForEach. En otras palabras, se debería preferir
source.AsParallel().Where(). ForAll(...) mucho antes que Parallel.ForEach(source.AsParallel().Where(), ...).
Ser consciente de los problemas de afinidad de los subprocesos
Algunas tecnologías, 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 código 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ó. Si intenta tener acceso al estado compartido de un control
Windows Forms en una consulta PLINQ, se produce una excepción si está ejecuta en el depurador.
(Este valor se puede desactivar.) Sin embargo, si la consulta se utiliza en el subproceso de la interfaz
de usuario, puede tener acceso al control desde el bucle foreach que enumera los resultados de la
consulta, ya que este código se ejecuta en un único subproceso.
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 tal vez
no tengan que ejecutarse en paralelo. Por consiguiente, se debe evitar escribir código cuya exactitud
dependa de la ejecución en paralelo de las iteraciones o de la ejecución de las iteraciones en algún
orden concreto.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 74 de 107
Por ejemplo, es probable que este código lleve a un interbloqueo:
Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, ProcessorCount * 100).AsParallel().ForAll(Sub(j)
If j = Environment.ProcessorCount Then
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()
End If
End Sub) ' deadlocks
En este ejemplo, una iteración 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
iteración 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 iteración del
valor de evento haya tenido oportunidad de ejecutarse. Esto produce un interbloqueo: la iteración
del valor de evento nunca se ejecutará y las iteraciones que esperan nunca se activarán.
En concreto, una iteración de un bucle paralelo no debe esperar nunca otra iteración del bucle para
progresar. Si el bucle paralelo decide programar las iteraciones secuencialmente pero en el orden
contrario, se producirá un interbloqueo.

2.6. Cómo: Crear y ejecutar una consulta PLINQ simple
En el siguiente ejemplo se muestra cómo crear un consulta LINQ paralela simple utilizando el método
de extensión AsParallel de la secuencia de origen y cómo ejecutarla utilizando el método ForAll(Of
TSource).
Ejemplo
Sub SimpleQuery()
Dim source = Enumerable.Range(100, 20000)
' Result sequence might be out of order.
Dim parallelQuery = From num In source.AsParallel()
Where num Mod 10 = 0
Select num
' Process result sequence in parallel
parallelQuery.ForAll(Sub(e)
DoSomething(e)
End Sub)
' Or use For Each to merge results first
' as in this example, Where results must
' be serialized sequentially through static Console method.
For Each n In parallelQuery
Console.Write("{0} ", n)
Next
' You can also use ToArray, ToList, etc
' as with LINQ to Objects.
Dim parallelQuery2 = (From num In source.AsParallel()
Where num Mod 10 = 0
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 75 de 107
Select num).ToArray()
'Method syntax is also supported
Dim parallelQuery3 = source.AsParallel().Where(Function(n)
Return (n Mod 10) = 0
End Function).Select(Function(n)
Return n
End Function)
For Each i As Integer In parallelQuery3
Console.Write("{0} ", i)
Next
Console.ReadLine()
End Sub
' A toy function to demonstrate syntax. Typically you need a more
' computationally expensive method to see speedup over sequential queries.
Sub DoSomething(ByVal i As Integer)
Console.Write("{0:###.## }", Math.Sqrt(i))
End Sub
En este ejemplo se muestra el modelo básico para crear y ejecutar cualquier consulta LINQ paralela
cuando la clasificación de la secuencia del resultado no es importante; las consultas no ordenadas
son generalmente más rápidas que las ordenadas. La consulta crea particiones del origen en tareas
que se ejecutan de forma asincrónica en varios subprocesos. El orden en que se completa cada tarea
depende no solo de la cantidad de trabajo que se precisa para procesar los elementos de la partición,
sino también de factores externos, como la forma en que el sistema operativo programa cada
subproceso., Se piensa que este ejemplo muestra el uso y no se puede ejecutar más rápidamente que
LINQ secuencial el equivalente a Objects consulte.

2.7. Cómo: Controlar la ordenación en una consulta PLINQ
En estos ejemplos se muestra cómo controlar la clasificación en una consulta PLINQ utilizando el
método de extensión AsOrdered.
Precaución
Se piensa principalmente que estos ejemplos muestran el uso y pueden o no se puede ejecutar más
rápidamente que LINQ secuencial el equivalente a las consultas de Objects.
Ejemplo
En el siguiente ejemplo se mantiene el orden de la secuencia de origen. A veces esto resulta
necesario, por ejemplo, cuando algunos operadores de consulta necesitan una secuencia de origen
ordenada para generar los resultados correctos.
Sub OrderedQuery()
Dim source = Enumerable.Range(9, 10000)
' Source is ordered let's preserve it.
Dim parallelQuery = From num In source.AsParallel().AsOrdered()
Where num Mod 3 = 0
Select num
' Use For Each to preserve order at execution time.
For Each item In parallelQuery
Console.Write("{0} ", item)
Next
' Some operators expect an ordered source sequence.
Dim lowValues = parallelQuery.Take(10)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 76 de 107
End Sub
En el siguiente ejemplo se muestran algunos operadores de consulta cuya secuencia de origen es
muy probable que esté ordenada. Estos operadores pueden funcionar en secuencias no ordenadas,
aunque pueden generar resultados inesperados.
' Paste into PLINQDataSample class
Shared Sub SimpleOrdering()
Dim customers As List(Of Customer) = GetCustomers().ToList()
' Take the first 20, preserving the original order
Dim firstTwentyCustomers = customers _
.AsParallel() _
.AsOrdered() _
.Take(20)
Console.WriteLine("Take the first 20 in original order")
For Each c As Customer In firstTwentyCustomers
Console.Write(c.CustomerID & " ")
Next
' All elements in reverse order.
Dim reverseOrder = customers _
.AsParallel() _
.AsOrdered() _
.Reverse()
Console.WriteLine(vbCrLf & "Take all elements in reverse order")
For Each c As Customer In reverseOrder
Console.Write("{0} ", c.CustomerID)
Next
' Get the element at a specified index.
Dim cust = customers.AsParallel() _
.AsOrdered() _
.ElementAt(48)
Console.WriteLine("Element #48 is: " & cust.CustomerID)
End Sub
Para ejecutar este método, péguelo en la clase PLINQDataSample del proyecto Ejemplo de datos de
PLINQ y presione F5.
En el ejemplo siguiente se muestra cómo se mantiene el orden en la primera parte de una consulta,
cómo se quita el orden para aumentar el rendimiento de una cláusula de combinación y cómo se
aplica de nuevo el orden a la secuencia del resultado final.
' Paste into PLINQDataSample class
Sub OrderedThenUnordered()
Dim Orders As IEnumerable(Of Order) = GetOrders()
Dim orderDetails As IEnumerable(Of OrderDetail) = GetOrderDetails()
' Sometimes it's easier to create a query
' by composing two subqueries
Dim query1 = From ord In Orders.AsParallel()
Where ord.OrderDate < DateTime.Parse("07/04/1997")
Select ord
Order By ord.CustomerID
Take 20
Dim query2 = From ord In query1.AsUnordered()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 77 de 107
Join od In orderDetails.AsParallel() On ord.OrderID Equals od.OrderID
Order By od.ProductID
Select New With {ord.OrderID, ord.CustomerID, od.ProductID}
For Each item In query2
Console.WriteLine("{0} {1} {2}", item.OrderID, item.CustomerID, item.ProductID)
Next
End Sub
Para ejecutar este método, péguelo en la clase PLINQDataSample del proyecto Ejemplo de datos de
PLINQ y presione F5.

2.8. Cómo: Combinar consultas LINQ paralelas y secuenciales
En este ejemplo se muestra cómo utilizar el método AsSequential(Of TSource) para indicarle a PLINQ
que procesar secuencialmente a todos los operadores subsiguientes en la consulta. Aunque el
procesamiento secuencial es por lo general más lento que el procesamiento en paralelo, a veces es
necesario para generar resultados correctos.
Ejemplo
En el siguiente ejemplo se muestra un escenario en el que se requiere AsSequential(Of TSource), a
saber, para conservar el orden que se estableció en una cláusula anterior de la consulta.
' Paste into PLINQDataSample class
Shared Sub SequentialDemo()
Dim orders = GetOrders()
Dim query = From ord In orders.AsParallel()
Order By ord.CustomerID
Select New With
{
ord.OrderID,
ord.OrderDate,
ord.ShippedDate
}
Dim query2 = query.AsSequential().Take(5)
For Each item In query2
Console.WriteLine("{0}, {1}, {2}", item.OrderDate, item.OrderID, item.ShippedDate)
Next
End Sub

2.9. Cómo: Controlar excepciones en una consulta PLINQ
En el primer ejemplo de este tema se muestra cómo controlar la excepción
System.AggregateException que se puede iniciar desde una consulta PLINQ cuando se ejecuta. En el
segundo ejemplo se muestra cómo incluir bloques try-catch dentro de los delegados, lo más
próximos posibles al punto donde se iniciará la excepción. De esta manera, se pueden detectar en
cuanto se producen y, posiblemente, continuar con la ejecución de la consulta. Cuando las
excepciones pueden propagarse de nuevo al subproceso de unión, es posible que una consulta
continúe procesando algunos elementos después de que se haya producido la excepción.
En algunos casos, cuando PLINQ utiliza la ejecución secuencial y se produce una excepción, es
posible que esta se propague directamente y no se encapsule en una excepción AggregateException.
Además, las excepciones ThreadAbortException siempre se propagan directamente.
Ejemplo
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 78 de 107
En este ejemplo se muestra cómo colocar los bloques try-catch alrededor del código que ejecuta la
consulta para detectar las excepciones System.AggregateExceptions que se produzcan.
' Paste into PLINQDataSample class
Shared Sub PLINQExceptions_1()
' Using the raw string array here. See PLINQ Data Sample.
Dim customers As String() = GetCustomersAsStrings().ToArray()
' First, we must simulate some currupt input.
customers(20) = "###"
'throws indexoutofrange
Dim query = From cust In customers.AsParallel() _
Let fields = cust.Split(","c) _
Where fields(3).StartsWith("C") _
Select fields
Try
' We use ForAll although it doesn't really improve performance
' since all output is serialized through the Console.
query.ForAll(Sub(e)
Console.WriteLine("City: {0}, Thread:{1}")
End Sub)
Catch e As AggregateException
' In this design, we stop query processing when the exception occurs.
For Each ex In e.InnerExceptions
Console.WriteLine(ex.Message)
If TypeOf ex Is IndexOutOfRangeException Then
Console.WriteLine("The data source is corrupt. Query stopped.")
End If
Next
End Try
End Sub
En este ejemplo, la consulta no puede continuar una vez producida la excepción. Cuando el código
de aplicación detecta la excepción, PLINQ ya ha detenido la consulta en todos los subprocesos.
En el siguiente ejemplo se muestra cómo colocar un bloque try-catch en un delegado para permitir
que se detecte una excepción y que continúe la ejecución de la consulta.
' Paste into PLINQDataSample class
Shared Sub PLINQExceptions_2()
Dim customers() = GetCustomersAsStrings().ToArray()
' Using the raw string array here.
' First, we must simulate some currupt input
customers(20) = "###"
' Create a delegate with a lambda expression.
' Assume that in this app, we expect malformed data
' occasionally and by design we just report it and continue.
Dim isTrue As Func(Of String(), String, Boolean) = Function(f, c)
Try
Dim s As String = f(3)
Return s.StartsWith(c)
Catch e As IndexOutOfRangeException
Console.WriteLine("Malformed cust: {0}", f)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 79 de 107
Return False
End Try
End Function
' Using the raw string array here
Dim query = From cust In customers.AsParallel()
Let fields = cust.Split(","c)
Where isTrue(fields, "C")
Select New With {.City = fields(3)}
Try
' We use ForAll although it doesn't really improve performance
' since all output must be serialized through the Console.
query.ForAll(Sub(e) Console.WriteLine(e.city))
' IndexOutOfRangeException will not bubble up
' because we handle it where it is thrown.
Catch e As AggregateException
For Each ex In e.InnerExceptions
Console.WriteLine(ex.Message)
Next
End Try
End

2.10. Cómo: Cancelar una consulta PLINQ
En los siguientes ejemplos se muestran dos maneras de cancelar una consulta PLINQ. En el primer
ejemplo se muestra cómo cancelar una consulta que consta principalmente de recorridos de datos.
En el segundo ejemplo se muestra cómo cancelar una consulta que contiene una función de usuario
que resulta cara en los cálculos.
Ejemplo
Class Program
Private Shared Sub Main(ByVal args As String())
Dim source As Integer() = Enumerable.Range(1, 10000000).ToArray()
Dim cs As New CancellationTokenSource()
' Start a new asynchronous task that will cancel the
' operation from another thread. Typically you would call
' Cancel() in response to a button click or some other
' user interface event.
Task.Factory.StartNew(Sub()
UserClicksTheCancelButton(cs)
End Sub)
Dim results As Integer() = Nothing
Try
results = (From num In source.AsParallel().WithCancellation(cs.Token) _
Where num Mod 3 = 0 _
Order By num Descending _
Select num).ToArray()
Catch e As OperationCanceledException
Console.WriteLine(e.Message)
Catch ae As AggregateException
If ae.InnerExceptions IsNot Nothing Then
For Each e As Exception In ae.InnerExceptions
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 80 de 107
Console.WriteLine(e.Message)
Next
End If
End Try
If results IsNot Nothing Then
For Each item In results
Console.WriteLine(item)
Next
End If
Console.WriteLine()
Console.ReadKey()
End Sub
Private Shared Sub UserClicksTheCancelButton(ByVal cs As CancellationTokenSource)
' Wait between 150 and 500 ms, then cancel.
' Adjust these values if necessary to make
' cancellation fire while query is still executing.
Dim rand As New Random()
Thread.Sleep(rand.[Next](150, 350))
cs.Cancel()
End Sub
End Class
El marco de PLINQ no incluye una única excepción OperationCanceledException en
System.AggregateException; OperationCanceledException se debe controlar en un bloque catch
independiente. Si uno o más delegados de usuario inician una excepción
OperationCanceledException(externalCT) (mediante una estructura
System.Threading.CancellationToken externa) pero ninguna otra excepción, y la consulta se definió
como AsParallel().WithCancellation(externalCT), PLINQ emitirá una única excepción
OperationCanceledException (externalCT) en lugar de System.AggregateException. Sin embargo, si
un delegado de usuario inicia una excepción OperationCanceledException y otro delegado inicia otro
tipo de excepción, ambas excepciones se incluyen en AggregateException.
A continuación se proporciona una orientación general sobre la cancelación:
1. Si realiza una cancelación del delegado de usuario, debería informar a PLINQ sobre la
estructura CancellationToken externa e iniciar una excepción OperationCanceledException
(externalCT).
2. Si se produce la cancelación y no se inicia ninguna otra excepción, debería controlar una
clase OperationCanceledException en lugar de AggregateException.
En el siguiente ejemplo se muestra cómo controlar la cancelación cuando se cuenta con una función
cara en cálculos en el código de usuario.
Class Program2
Private Shared Sub Main(ByVal args As String())
Dim source As Integer() = Enumerable.Range(1, 10000000).ToArray()
Dim cs As New CancellationTokenSource()
' Start a new asynchronous task that will cancel the operation from another thread. Typically you would call
' Cancel() in response to a button click or some other user interface event.
Task.Factory.StartNew(Sub()
UserClicksTheCancelButton(cs)
End Sub)
Dim results As Double() = Nothing
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 81 de 107
Try
results = (From num In source.AsParallel().WithCancellation(cs.Token) _
Where num Mod 3 = 0 _
Select [Function](num, cs.Token)).ToArray()
Catch e As OperationCanceledException
Console.WriteLine(e.Message)
Catch ae As AggregateException
If ae.InnerExceptions IsNot Nothing Then
For Each e As Exception In ae.InnerExceptions
Console.WriteLine(e.Message)
Next
End If
End Try
If results IsNot Nothing Then
For Each item In results
Console.WriteLine(item)
Next
End If
Console.WriteLine()
Console.ReadKey()
End Sub
' A toy method to simulate work.
Private Shared Function [Function](ByVal n As Integer, ByVal ct As CancellationToken) As Double
' If work is expected to take longer than 1 ms then try to check cancellation status more
' often within that work.
For i As Integer = 0 To 4
' Work hard for approx 1 millisecond.
Thread.SpinWait(50000)
' Check for cancellation request.
If ct.IsCancellationRequested Then
Throw New OperationCanceledException(ct)
End If
Next
' Anything will do for our purposes.
Return Math.Sqrt(n)
End Function
Private Shared Sub UserClicksTheCancelButton(ByVal cs As CancellationTokenSource)
' Wait between 150 and 500 ms, then cancel.
' Adjust these values if necessary to make cancellation fire while query is still executing.
Dim rand As New Random()
Thread.Sleep(rand.[Next](150, 350))
Console.WriteLine("Press 'c' to cancel")
If Console.ReadKey().KeyChar = "c"c Then
cs.Cancel()
End If
End Sub
End Class
Al administrar la cancelación en código de usuario, no tiene que utilizar WithCancellation(Of TSource)
en la definición de la consulta. Sin embargo, recomendamos hacer esto porque WithCancellation(Of
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 82 de 107
TSource) no tiene ningún efecto en rendimiento de las consultas y permite administrar la cancelación
por operadores de consulta y su código de usuario.
Para garantizar la capacidad de respuesta del sistema, se recomienda comprobar la cancelación
alrededor de una vez por milisegundo; también se considera aceptable, sin embargo, un período
máximo de 10 milisegundos. Esta frecuencia no tiene por qué tener un impacto negativo en el
rendimiento del código.
Cuando se dispone un enumerador, por ejemplo cuando el código se escapa de un bucle de foreach
(Para Cada uno en Visual Basic) que está iterando sobre los resultados de la consulta, a continuación,
la consulta está cancelada, pero no se produce ninguna excepción.

2.11. Cómo: Escribir una función de agregado personalizada de PLINQ
En este ejemplo se muestra cómo utilizar el método Aggregate para aplicar una función de
agregación personalizada a una secuencia del origen.
Ejemplo
En el siguiente ejemplo se calcula la desviación estándar de una secuencia de enteros.
Class aggregation
Private Shared Sub Main(ByVal args As String())
' Create a data source for demonstration purposes.
Dim source As Integer() = New Integer(99999) {}
Dim rand As New Random()
For x As Integer = 0 To source.Length - 1
' Should result in a mean of approximately 15.0.
source(x) = rand.[Next](10, 20)
Next
' Standard deviation calculation requires that we first calculate the mean average. Average is a predefined
' aggregation operator, along with Max, Min and Count.
Dim mean As Double = source.AsParallel().Average()
' We use the overload that is unique to ParallelEnumerable. The third Func parameter combines the results from
each
' thread. initialize subtotal. Use decimal point to tell the compiler this is a type double. Can also use: 0d.
' do this on each thread aggregate results after all threads are done.
' perform standard deviation calc on the aggregated result.
Dim standardDev As Double = source.AsParallel().Aggregate(0.0R, Function(subtotal, item) subtotal +
Math.Pow((item - mean), 2), Function(total, thisThread) total + thisThread, Function(finalSum) Math.Sqrt((finalSum /
(source.Length - 1))))
Console.WriteLine("Mean value is = {0}", mean)
Console.WriteLine("Standard deviation is {0}", standardDev)
Console.ReadLine()
End Sub
End Class
En este ejemplo se utiliza una sobrecarga del operador de consulta estándar Aggregate, que solo
existe en PLINQ. Esta sobrecarga toma un delegado System.Func(Of T1, T2, TResult) adicional como
tercer parámetro de entrada. Este delegado combina los resultados de todos los subprocesos antes
de realizar el cálculo final en los resultados agregados. En este ejemplo sumamos las sumas de todos
los subprocesos.
Tenga en cuenta que cuando el cuerpo de una expresión lambda está compuesto por una única
expresión, el valor devuelto del delegado System.Func(Of T, TResult) es el valor de la expresión.

2.12. Cómo: Especificar el modo de ejecución en PLINQ
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 83 de 107
En este ejemplo se muestra cómo forzar a PLINQ a omitir su heurística predeterminada y ejecutar en
paralelo una consulta sin tener en cuenta la forma de la consulta.
Ejemplo
Private Shared Sub ForceParallel()
Dim customers = GetCustomers()
Dim parallelQuery = (From cust In
customers.AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism) _
Where cust.City = "Berlin" _
Select cust.CustomerName).ToList()
End Sub
PLINQ está diseñado para aprovechar las oportunidades para la ejecución en paralelo. Sin embargo,
no todas las consultas se benefician de la ejecución en paralelo. Por ejemplo, cuando una consulta
contiene un delegado de usuario único que hace muy poco trabajo, la consulta normalmente se
ejecutará más rápidamente de forma secuencial. Esto se debe a que la sobrecarga necesaria para
habilitar la ejecución en paralelo es más costosa que la velocidad que se obtiene. Por consiguiente,
PLINQ no ejecuta en paralelo cada consulta de forma automática. Primero examina la forma de la
consulta y los diversos operadores que la comprenden. En función de este análisis, PLINQ en el modo
de ejecución predeterminado puede decidir ejecutar algunas consultas o todas ellas
secuencialmente. Sin embargo, en algunos casos puede saber sobre su consulta más de lo que PLINQ
puede determinar a partir de su análisis. Por ejemplo, puede saber que un delegado es muy costoso
y que la consulta se beneficiará definitivamente de la ejecución en paralelo. En casos como éste,
puede utilizar el método WithExecutionMode(Of TSource) y especificar el valor ForceParallelism para
indicarle a PLINQ que ejecutar siempre la consulta como paralelo.

2.13. Cómo: Especificar opciones de combinación en PLINQ
En este ejemplo se muestra cómo especificar las opciones de combinación que se aplicarán a todos
los operadores subsiguientes en una consulta PLINQ. No tiene que establecer las opciones de
combinación explícitamente, pero, si lo hace, puede mejorar rendimiento.
Ejemplo
En el siguiente ejemplo se muestra el comportamiento de las opciones de combinación en un
escenario básico que tiene un origen no ordenado y aplica una función que utiliza muchos recursos a
cada elemento.
Class MergeOptions2
Sub DoMergeOptions()
Dim nums = Enumerable.Range(1, 10000)
' Replace NotBuffered with AutoBuffered
' or FullyBuffered to compare behavior.
Dim scanLines = From n In nums.AsParallel().WithMergeOptions(ParallelMergeOptions.NotBuffered)
Where n Mod 2 = 0
Select ExpensiveFunc(n)
Dim sw = System.Diagnostics.Stopwatch.StartNew()
For Each line In scanLines
Console.WriteLine(line)
Next
Console.WriteLine("Elapsed time: {0} ms. Press any key to exit.")
Console.ReadKey()
End Sub
' A function that demonstrates what a fly sees when it watches television :-)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 84 de 107
Function ExpensiveFunc(ByVal i As Integer) As String
System.Threading.Thread.SpinWait(2000000)
Return String.Format("{0} *****************************************", i)
End Function
End Class
En casos donde la opción AutoBuffered incurre en una latencia indeseable antes de que se produzca
el primer elemento, pruebe la opción NotBuffered para producir los elementos de resultado más
rápido y más fácilmente.

2.14. Cómo: Recorrer en iteración directorios con PLINQ
En este ejemplo se muestran dos maneras simples de paralelizar las operaciones en directorios de
archivo. La primera consulta utiliza el método GetFiles para rellenar una matriz de nombres de
archivo en un directorio y todos los subdirectorios. Este método no devuelve hasta que se rellene la
matriz completa, y por consiguiente puede introducir al principio la latencia de la operación. Sin
embargo, una vez rellenada la matriz, PLINQ puede procesarlo muy rápidamente en paralelo.
Los segundos usos de la consulta en los métodos EnumerateFiles y los EnumerateDirectories
estáticos que empiezan a devolver los resultados inmediatamente. Este enfoque puede ser más
rápido cuando está iterando sobre los árboles de directorios grandes, aunque el tiempo de proceso
comparó al primer ejemplo puede depender de muchos factores.
Ejemplo
El siguiente ejemplo muestra cómo iterar sobre los directorios de archivo en escenarios simples al
tener el acceso a todos los directorios en el árbol, los tamaños de archivo no son muy grandes y los
tiempos de acceso no son significativos. Este enfoque implica un período de latencia al principio
mientras se construye la matriz de nombres de archivo.
El siguiente ejemplo muestra cómo iterar sobre los directorios de archivo en escenarios simples al
tener el acceso a todos los directorios en el árbol, los tamaños de archivo no son muy grandes y los
tiempos de acceso no son significativos. Este enfoque empieza a generar resulta más rápidamente
que el ejemplo anterior.
Este idioma no es compatible o no hay ningún ejemplo de código disponible.
Al utilizar GetFiles, asegurarse de tener los permisos necesarios en todos los directorios en el árbol.
De lo contrario se producirá una excepción y no se devolverá ningún resultado. Al utilizar
EnumerateDirectories en una consulta PLINQ, es problemático para administrar las excepciones I/O
de una manera elegante que le permite seguir iterando. Si su código debe administrar excepciones
de acceso no autorizado o E/S, a continuación, debería considerar el enfoque descrito en Cómo:
Recorrer en iteración directorios con la clase paralela.
If I/O latency is an issue, for example with file I/O over a network, consider using one of the
asynchronous I/O techniques described in TPL y la programación asincrónica tradicional de .NET.

2.15. Cómo: Medir el rendimiento de consultas PLINQ
En este ejemplo se muestra cómo usar la clase Stopwatch para medir el tiempo que tarda en
ejecutarse una consulta PLINQ.
Ejemplo
En este ejemplo se usa un bucle foreach vacío (For Each en Visual Basic) para medir el tiempo que
tarda en ejecutarse la consulta. En el código real, normalmente, el bucle contiene pasos de
procesamiento adicionales que aumentan el tiempo de ejecución total de la consulta. Observe que el
cronómetro se inicia justo antes del bucle, porque es en ese momento cuando comienza la ejecución
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 85 de 107
de la consulta. Si necesita una medición más exacta, puede usar la propiedad ElapsedTicks en lugar
de ElapsedMilliseconds.
Sub Main()
Dim source = Enumerable.Range(0, 3000000)
Dim queryToMeasure = From num In source
Where num Mod 3 = 0
Select Math.Sqrt(num)
Console.WriteLine("Measuring...")
' The query does not run until it is enumerated.
' Therefore, start the timer here.
Dim sw = System.Diagnostics.Stopwatch.StartNew()
' For pure query cost, enumerate and do nothing else.
For Each n As Double In queryToMeasure
Next
Dim elapsed As Long
elapsed = sw.ElapsedMilliseconds ' or sw.ElapsedTicks
Console.WriteLine("Total query time: {0} ms.", elapsed)
' Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
El tiempo de ejecución total es una medida útil cuando se prueban implementaciones de consultas,
pero no siempre lo dice todo. Para obtener una vista más detallada y completa de la interacción de
los subprocesos de consulta entre sí y con otros procesos en ejecución, use el visualizador de
simultaneidad. Esta herramienta está disponible en Microsoft Visual Studio 2010 Premium.

2.16. Ejemplo de datos de PLINQ
Este ejemplo contiene datos de ejemplo en formato .csv, junto con métodos que lo transforman en
colecciones en memoria de clientes, productos, pedidos y detalles de pedidos. Si desea realizar más
pruebas con PLINQ, puede pegar ejemplos de código de otros temas en el código de este tema e
invocarlo desde el método Main. También puede usar estos datos con sus propias consultas PLINQ.
Los datos representan un subconjunto de la base de datos Northwind. Se incluyen cincuenta (50)
registros de cliente, pero no todos los campos. Se incluye un subconjunto de las filas de pedidos y
los detalles correspondientes de Order_Detail de cada cliente. Se incluyen todos los productos.
Para configurar este ejemplo
1. Cree un proyecto de aplicación de consola de Visual Basic o Visual C#.
2. Reemplace el contenido de Module1.vb o Program.cs utilizando el código que se incluye
después de estos pasos.
3. En el menú Proyecto, haga clic en Agregar nuevo elemento. Seleccione Archivo de texto
y, a continuación, haga clic en Aceptar. Copie los datos de este tema y, a continuación,
péguelos en el nuevo archivo de texto. En el menú Archivo, haga clic en Guardar, asigne al
archivo el nombre Plinqdata.csv y, a continuación, guárdelo en la carpeta que contiene sus
archivos de código fuente.
4. Presione F5 para comprobar que el proyecto se compila y se ejecuta correctamente. Se
debería mostrar la salida siguiente en la ventana de la consola.
Customer count: 50
Product count: 77
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 86 de 107
Order count: 190
Order Details count: 483
Press any key to exit.
' This class contains a subset of data from the Northwind database
' in the form of string arrays. The methods such as GetCustomers, GetOrders, and so on
' transform the strings into object arrays that you can query in an object-oriented way.
' Many of the code examples in the PLINQ How-to topics are designed to be pasted into
' the PLINQDataSample class and invoked from the Main method.
' IMPORTANT: This data set is not large enough for meaningful comparisons of PLINQ vs. LINQ performance.
Class PLINQDataSample
Shared Sub Main(ByVal args As String())
'Call methods here.
TestDataSource()
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Shared Sub TestDataSource()
Console.WriteLine("Customer count: {0}", GetCustomers().Count())
Console.WriteLine("Product count: {0}", GetProducts().Count())
Console.WriteLine("Order count: {0}", GetOrders().Count())
Console.WriteLine("Order Details count: {0}", GetOrderDetails().Count())
End Sub
#Region "DataClasses"
Class Order
Public Sub New()
_OrderDetails = New Lazy(Of OrderDetail())(Function() GetOrderDetailsForOrder(OrderID))
End Sub
Private _OrderID As Integer
Public Property OrderID() As Integer
Get
Return _OrderID
End Get
Set(ByVal value As Integer)
_OrderID = value
End Set
End Property
Private _CustomerID As String
Public Property CustomerID() As String
Get
Return _CustomerID
End Get
Set(ByVal value As String)
_CustomerID = value
End Set
End Property
Private _OrderDate As DateTime
Public Property OrderDate() As DateTime
Get
Return _OrderDate
End Get
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 87 de 107
Set(ByVal value As DateTime)
_OrderDate = value
End Set
End Property
Private _ShippedDate As DateTime
Public Property ShippedDate() As DateTime
Get
Return _ShippedDate
End Get
Set(ByVal value As DateTime)
_ShippedDate = value
End Set
End Property
Private _OrderDetails As Lazy(Of OrderDetail())
Public ReadOnly Property OrderDetails As OrderDetail()
Get
Return _OrderDetails.Value
End Get
End Property
End Class
Class Customer
Private _Orders As Lazy(Of Order())
Public Sub New()
_Orders = New Lazy(Of Order())(Function() GetOrdersForCustomer(_CustomerID))
End Sub
Private _CustomerID As String
Public Property CustomerID() As String
Get
Return _CustomerID
End Get
Set(ByVal value As String)
_CustomerID = value
End Set
End Property
Private _CustomerName As String
Public Property CustomerName() As String
Get
Return _CustomerName
End Get
Set(ByVal value As String)
_CustomerName = value
End Set
End Property
Private _Address As String
Public Property Address() As String
Get
Return _Address
End Get
Set(ByVal value As String)
_Address = value
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 88 de 107
End Set
End Property
Private _City As String
Public Property City() As String
Get
Return _City
End Get
Set(ByVal value As String)
_City = value
End Set
End Property
Private _PostalCode As String
Public Property PostalCode() As String
Get
Return _PostalCode
End Get
Set(ByVal value As String)
_PostalCode = value
End Set
End Property
Public ReadOnly Property Orders() As Order()
Get
Return _Orders.Value
End Get
End Property
End Class
Class Product
Private _ProductName As String
Public Property ProductName() As String
Get
Return _ProductName
End Get
Set(ByVal value As String)
_ProductName = value
End Set
End Property
Private _ProductID As Integer
Public Property ProductID() As Integer
Get
Return _ProductID
End Get
Set(ByVal value As Integer)
_ProductID = value
End Set
End Property
Private _UnitPrice As Double
Public Property UnitPrice() As Double
Get
Return _UnitPrice
End Get
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 89 de 107
Set(ByVal value As Double)
_UnitPrice = value
End Set
End Property
End Class
Class OrderDetail
Private _OrderID As Integer
Public Property OrderID() As Integer
Get
Return _OrderID
End Get
Set(ByVal value As Integer)
_OrderID = value
End Set
End Property
Private _ProductID As Integer
Public Property ProductID() As Integer
Get
Return _ProductID
End Get
Set(ByVal value As Integer)
_ProductID = value
End Set
End Property
Private _UnitPrice As Double
Public Property UnitPrice() As Double
Get
Return _UnitPrice
End Get
Set(ByVal value As Double)
_UnitPrice = value
End Set
End Property
Private _Quantity As Double
Public Property Quantity() As Double
Get
Return _Quantity
End Get
Set(ByVal value As Double)
_Quantity = value
End Set
End Property
Private _Discount As Double
Public Property Discount() As Double
Get
Return _Discount
End Get
Set(ByVal value As Double)
_Discount = value
End Set
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 90 de 107
End Property
End Class
#End Region
Shared Function GetCustomersAsStrings() As IEnumerable(Of String)
Return System.IO.File.ReadAllLines("..\..\plinqdata.csv").
SkipWhile(Function(line) line.StartsWith("CUSTOMERS") = False).
Skip(1).
TakeWhile(Function(line) line.StartsWith("END CUSTOMERS") = False)
End Function
Shared Function GetCustomers() As IEnumerable(Of Customer)
Dim customers = System.IO.File.ReadAllLines("..\..\plinqdata.csv").
SkipWhile(Function(line) line.StartsWith("CUSTOMERS") = False).
Skip(1).
TakeWhile(Function(line) line.StartsWith("END CUSTOMERS") = False)
Return From line In customers
Let fields = line.Split(","c)
Select New Customer With {
.CustomerID = fields(0).Trim(),
.CustomerName = fields(1).Trim(),
.Address = fields(2).Trim(),
.City = fields(3).Trim(),
.PostalCode = fields(4).Trim()}
End Function
Shared Function GetOrders() As IEnumerable(Of Order)
Dim orders = System.IO.File.ReadAllLines("..\..\plinqdata.csv").
SkipWhile(Function(line) line.StartsWith("ORDERS") = False).
Skip(1).
TakeWhile(Function(line) line.StartsWith("END ORDERS") = False)
Return From line In orders
Let fields = line.Split(","c)
Select New Order With {
.OrderID = CType(fields(0).Trim(), Integer),
.CustomerID = fields(1).Trim(),
.OrderDate = DateTime.Parse(fields(2)),
.ShippedDate = DateTime.Parse(fields(3))}
End Function
Shared Function GetOrdersForCustomer(ByVal id As String) As Order()
Dim orders = System.IO.File.ReadAllLines("..\..\plinqdata.csv").
SkipWhile(Function(line) line.StartsWith("ORDERS") = False).
Skip(1).
TakeWhile(Function(line) line.StartsWith("END ORDERS") = False)
Dim orderStrings = From line In orders
Let fields = line.Split(","c)
Let custID = fields(1).Trim()
Where custID = id
Select New Order With {
.OrderID = CType(fields(0).Trim(), Integer),
.CustomerID = custID,
.OrderDate = DateTime.Parse(fields(2)),
.ShippedDate = DateTime.Parse(fields(3))}
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 91 de 107
Return orderStrings.ToArray()
End Function
Shared Function GetProducts() As IEnumerable(Of Product)
Dim products = System.IO.File.ReadAllLines("..\..\plinqdata.csv").
SkipWhile(Function(line) line.StartsWith("PRODUCTS") = False).
Skip(1).
TakeWhile(Function(line) line.StartsWith("END PRODUCTS") = False)
Return From line In products
Let fields = line.Split(","c)
Select New Product With {
.ProductID = CType(fields(0), Integer),
.ProductName = fields(1).Trim(),
.UnitPrice = CType(fields(2), Double)}
End Function
Shared Function GetOrderDetailsForOrder(ByVal orderID As Integer) As OrderDetail()
Dim orderDetails = System.IO.File.ReadAllLines("..\..\plinqdata.csv").
SkipWhile(Function(line) line.StartsWith("ORDER DETAILS") = False).
Skip(1).
TakeWhile(Function(line) line.StartsWith("END ORDER DETAILS") = False)
Dim ordDetailStrings = From line In orderDetails
Let fields = line.Split(","c)
Let ordID = Convert.ToInt32(fields(0))
Where ordID = orderID
Select New OrderDetail With {
.OrderID = ordID,
.ProductID = CType(fields(1), Integer),
.UnitPrice = CType(fields(2), Double),
.Quantity = CType(fields(3), Double),
.Discount = CType(fields(4), Double)}
Return ordDetailStrings.ToArray()
End Function
Shared Function GetOrderDetails() As IEnumerable(Of OrderDetail)
Dim orderDetails = System.IO.File.ReadAllLines("..\..\plinqdata.csv").
SkipWhile(Function(line) line.StartsWith("ORDER DETAILS") = False).
Skip(1).
TakeWhile(Function(line) line.StartsWith("END ORDER DETAILS") = False)
Return From line In orderDetails
Let fields = line.Split(","c)
Select New OrderDetail With {
.OrderID = CType(fields(0), Integer),
.ProductID = CType(fields(1), Integer),
.UnitPrice = CType(fields(2), Double),
.Quantity = CType(fields(3), Double),
.Discount = CType(fields(4), Double)}
End Function
End Class

3. Estructuras de datos para la programación paralela
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 92 de 107
.NET Framework versión 4 incorpora varios tipos nuevos que resultan útiles para la programación en
paralelo, entre los que se incluye un conjunto de clases de colección simultáneas, primitivas de
sincronización ligeras y tipos de inicialización diferida. Puede usar estos tipos con cualquier código
de aplicación multiproceso, incluso con la biblioteca TPL (Task Parallel Library, biblioteca de
procesamiento paralelo basado en tareas) y PLINQ.
Clases de colección simultáneas
Las clases de colección del espacio de nombres System.Collections.Concurrent proporcionan
operaciones Add y Remove seguras para subprocesos que evitan los bloqueos en la medida de lo
posible y, cuando es necesario, usan bloqueos específicos. A diferencia de las colecciones que se
incorporaron en las versiones 1.0 y 2.0 de .NET Framework, una clase de colección simultánea no
requiere que el código de usuario tome ningún bloqueo cuando obtiene acceso a los elementos. Las
clases de colección simultáneas pueden mejorar significativamente el rendimiento frente a tipos
como System.Collections.ArrayList y System.Collections.Generic.List(Of T) (con bloqueo
implementado por el usuario) en escenarios en lo que varios subprocesos agregan y quitan
elementos de una colección.
En la tabla siguiente se muestran las nuevas clases de colección simultáneas:
Escriba Descripción
System.Collections.Concurrent.BlockingCollection(Of T)
Proporciona capacidades de bloqueo y establecimiento de
límites en colecciones seguras para subprocesos que
implementan
System.Collections.Concurrent.IProducerConsumerCollection(Of
T). Los subprocesos de productor se bloquean si no hay ranuras
disponibles o si la colección está completa. Los subprocesos de
consumidor se bloquean si la colección está vacía. Este tipo
también permite el acceso sin bloqueo de los subprocesos de
consumidor y productor. BlockingCollection(Of T) puede usarse
como clase base o dispositivo de copia de seguridad para
proporcionar el bloqueo y el establecimiento de límites de
cualquier clase de colección que admita IEnumerable(Of T).
System.Collections.Concurrent.ConcurrentBag(Of T)
Implementación de un contenedor seguro para subprocesos
que proporciona operaciones Add y Get escalables.
System.Collections.Concurrent.ConcurrentDictionary(Of
TKey, TValue)
Tipo de diccionario simultáneo y escalable.
System.Collections.Concurrent.ConcurrentQueue(Of T) Cola FIFO simultánea y escalable.
System.Collections.Concurrent.ConcurrentStack(Of T) Pila LIFO simultánea y escalable.
Primitivas de sincronización
Las nuevas primitivas de sincronización del espacio de nombres System.Threading proporcionan una
simultaneidad específica y un mayor rendimiento ya que evitan los costosos mecanismos de bloqueo
que se encuentran en el código multithreading heredado. Algunos de los nuevos tipos, como
System.Threading.Barrier y System.Threading.CountdownEvent, no tienen equivalente en versiones
anteriores de .NET Framework.
En la tabla siguiente, se muestran los nuevos tipos de sincronización:
Escriba Descripción
System.Threading.Barrier
Permite que varios subprocesos trabajen en un algoritmo
en paralelo proporcionando un punto en el que cada tarea
puede señalar su llegada y bloquearse hasta que algunas o
todas las tareas hayan llegado.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 93 de 107
System.Threading.CountdownEvent
Simplifica los escenarios de bifurcación y unión
proporcionando un mecanismo de encuentro sencillo.
System.Threading.ManualResetEventSlim
Primitiva de sincronización similar a
System.Threading.Manual ResetEvent.
ManualResetEventSlim es un objeto ligero, pero solo puede
usarse en la comunicación que tiene lugar dentro de un
proceso.
System.Threading.SemaphoreSlim
Primitiva de sincronización que limita el número de
subprocesos que pueden obtener acceso a la vez a un
recurso o grupo de recursos.
System.Threading.SpinLock
Primitiva de bloqueo de exclusión mutua que hace que el
subproceso que está intentando adquirir el bloqueo espere
en un bucle o ciclo durante un período de tiempo antes de
que se produzca su cuanto. En escenarios en los que se
prevé que la espera del bloqueo será breve, SpinLock
proporciona mayor rendimiento que otras formas de
bloqueo.
System.Threading.SpinWait
Tipo pequeño y ligero que iterará en ciclos durante un
período especificado y situará el subproceso en estado de
espera si el recuento de ciclos se supera.
Cómo: Utilizar SpinLock para la sincronización de bajo nivel
Cómo: Sincronizar operaciones simultáneas con una clase Barrier.
Clases de inicialización diferida
Con la inicialización diferida, la memoria de un objeto no se asigna hasta que es necesario. La
inicialización diferida puede mejorar el rendimiento al extender las asignaciones de objetos
uniformemente a lo largo de la duración de un programa. Puede habilitar la inicialización diferida en
cualquier tipo personalizado encapsulando el tipo Lazy(Of T).
En la tabla siguiente, se muestran los tipos de inicialización diferida:
Escriba Descripción
System.Lazy(Of T)
Proporciona una inicialización diferida ligera y segura para
subprocesos.
System.Threading.ThreadLocal(Of
T)
Proporciona un valor de inicialización diferida por cada
subproceso, donde cada subproceso invoca de forma diferida la
función de inicialización.
System.Threading.LazyInitializer
Proporciona métodos estáticos que evitan tener que asignar una
instancia de inicialización diferida dedicada. En su lugar, usan
referencias para garantizar que los destinos se han inicializado a
medida que se va obteniendo acceso a ellos.
Excepciones agregadas
El tipo System.AggregateException se puede utilizar para capturar varias excepciones que se
producen simultáneamente en diferentes subprocesos y devolverlas al subproceso de unión como
una sola excepción. Los tipos System.Threading.Tasks.Task y System.Threading.Tasks.Parallel así como
la PLINQ usan AggregateException en gran medida con este propósito.

4. Herramientas de diagnóstico paralelo
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 94 de 107
Microsoft Visual Studio 2010 proporciona amplia compatibilidad para depurar y generar los perfiles
de aplicaciones multithreading.
Depuración
El depurador de Visual Studio agrega nuevas ventanas para depurar aplicaciones paralelas.
Generación de perfiles
En las vistas de informe del visualizador de simultaneidad se puede ver cómo los subprocesos de un
programa paralelo interactúan entre sí y con los subprocesos de otros procesos del sistema.
5. Particionadores personalizados para PLINQ y TPL
Para paralelizar una operación en un origen de datos, uno de los pasos esenciales es crear particiones
del origen en varias secciones a las que se tenga acceso simultáneamente a través de varios
subprocesos. PLINQ y Task Parallel Library (TPL) proporcionan particionadores predeterminados que
funcionan de forma transparente al escribir una consulta o bucle ForEach en paralelo. En escenarios
más avanzados, puede conectar su propio particionador.
Creación de particiones diferentes
Hay muchas maneras de crear particiones de un origen de datos. En los enfoques más eficaces, varios
subprocesos cooperan para procesar la secuencia original, en lugar de separar físicamente el origen
en varias subsecuencias. Para matrices y otros orígenes indizados como colecciones IList donde de
antemano se conoce la longitud, la creación de particiones por intervalos es la forma más simple de
crear particiones. Cada subproceso recibe índices iniciales y finales únicos, para poder procesar el
intervalo del origen sin sobrescribir ni ser sobrescrito por otro subproceso. La única sobrecarga
implicada en la creación de particiones por intervalos es el trabajo inicial de crear los intervalos;
ninguna sincronización adicional se requiere después de eso. Por consiguiente, puede proporcionar
buen rendimiento siempre que la carga de trabajo se divida uniformemente. Una desventaja de la
creación de particiones por intervalos es que si un subproceso finaliza pronto, no puede ayudar a los
demás a finalizar el trabajo.
Con listas vinculadas u otras colecciones cuya longitud no se conoce, puede utilizar la creación de
particiones por fragmentos. En la creación de particiones por fragmentos, cada subproceso o tarea de
un bucle o consulta en paralelo utiliza un número de elementos de origen de un fragmento, los
procesa y vuelve a recuperar más elementos. Los particionadores se aseguran de que se distribuyen
todos los elementos y no hay ningún duplicado. Un fragmento puede tener cualquier tamaño. Por
ejemplo, el particionador que se muestra en Cómo: Implementar las particiones dinámicas crea
fragmentos que contienen un solo elemento. Con tal de que los fragmentos no sean demasiado
grandes, esta forma de crear particiones mantiene inherentemente el equilibrio de carga porque la
asignación de elementos a subprocesos no está predeterminada. Sin embargo, el particionador
incurre en la sobrecarga de sincronización cada vez que el subproceso necesita obtener otro
fragmento. La cantidad de sincronización en que se incurre en estos casos es inversamente
proporcional al tamaño de los fragmentos.
En general, la creación de particiones por intervalos solo es más rápida cuando el tiempo de
ejecución del delegado es de poco a moderado, el origen tiene un número grande de elementos y el
trabajo total de cada partición es aproximadamente equivalente. La creación de particiones por
fragmentos es, por consiguiente, más rápida en la mayoría de los casos. En orígenes con un número
pequeño de elementos o tiempos de ejecución más largos para el delegado, el rendimiento de la
creación de particiones por fragmentos e intervalos es casi igual.
Los particionadores de TPL también admiten un número de particiones dinámicas. Esto significa que
pueden crear particiones sobre la marcha, por ejemplo, cuando el bucle ForEach genera una nueva
tarea. Esta característica permite al particionador escalar junto con el propio bucle. Los
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 95 de 107
particionadores dinámicos también mantienen inherentemente el equilibrio de carga. Cuando se crea
un particionador personalizado, se debe admitir que la creación de particiones dinámicas se use
desde un bucle ForEach.
Configurar particionadores de equilibrio de carga para PLINQ
Algunas sobrecargas del método Partitioner.Create permitían crear particionadores para una matriz u
origen IList y especificar si debía intentar equilibrar la carga de trabajo entre los subprocesos. Cuando
se configura el particionador para equilibrar la carga, se utiliza la creación de particiones por
fragmentos y los elementos se presentan fuera de cada partición en pequeños fragmentos a medida
que se solicitan. Este enfoque ayuda a asegurar que todas las particiones tienen elementos para
procesar hasta que todo el bucle o la consulta se completa. Se puede utilizar una sobrecarga
adicional para proporcionar la creación de particiones de equilibrio de carga de cualquier origen
IEnumerable.
En general, el equilibrio de carga exige que las particiones soliciten elementos con relativa frecuencia
de los particionadores. Por contraste, el particionador que crea particiones estáticas puede asignar
todos los elementos a la vez mediante la creación de particiones por intervalos o por fragmentos.
Esto requiere menos sobrecarga que el equilibrio de carga, pero podría llevar más mucho tiempo
ejecutarse si un subproceso termina significativamente con más trabajo que los demás. De forma
predeterminada cuando se pasa IList o una matriz, PLINQ siempre utiliza la creación de particiones
por intervalos sin equilibrio de carga. Para habilitar el equilibrio de carga para PLINQ, use el método
Partitioner.Create, como se muestra en el siguiente ejemplo.
' Static number of partitions requires indexable source.
Dim nums = Enumerable.Range(0, 100000000).ToArray()
' Create a load-balancing partitioner. Or specify false For Shared partitioning.
Dim customPartitioner = Partitioner.Create(nums, True)
' The partitioner is the query's data source.
Dim q = From x In customPartitioner.AsParallel()
Select x * Math.PI
q.ForAll(Sub(x) ProcessData(x))
La mejor manera de determinar si utilizar el equilibrio de carga en un escenario determinado es
experimentar y medir cuánto tiempo tardan las operaciones en completarse con cargas y
configuraciones de equipo representativas. Por ejemplo, la creación de particiones estáticas podría
proporcionar un aumento de velocidad significativo en un equipo multiproceso con pocos núcleos,
pero podría ralentizar los equipos que tienen relativamente más núcleos.
En la tabla siguiente se muestran las sobrecargas disponibles del método Create. Estos
particionadores no están limitados a su uso con PLINQ o ForEach. También se pueden utilizar con
cualquier construcción paralela personalizada.
Sobrecarga Utiliza el equilibrio de carga
Create(Of TSource)(IEnumerable(Of
TSource))
Siempre
Create(Of TSource)(TSource(), Boolean)
Cuando el argumento booleano se especifica como
verdadero
Create(Of TSource)(IList(Of TSource),
Boolean)
Cuando el argumento booleano se especifica como
verdadero
Create(Int32, Int32) Nunca
Create(Int32, Int32, Int32) Nunca
Create(Int64, Int64) Nunca
Create(Int64, Int64, Int64) Nunca
Configurar particionadores por intervalos estáticos para Parallel.ForEach
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 96 de 107
En un bucle For, el cuerpo del bucle se proporciona al método como un delegado. El costo de invocar
ese delegado es más o menos similar a una llamada al método virtual. En algunos escenarios, el
cuerpo de un bucle paralelo podría ser lo bastante pequeño como para que el costo de la invocación
del delegado en cada iteración del bucle fuera significativa. En tales situaciones, puede utilizar una de
las sobrecargas Create para crear una IEnumerable(Of T) de particiones por intervalos de los
elementos de origen. Después puede pasar esta colección de intervalos a un método ForEach cuyo
cuerpo está compuesto de un bucle for normal. La ventaja de este enfoque es que solo se incurre en
el costo de invocación de delegados una vez por intervalo, en lugar de una vez por elemento. En el
siguiente ejemplo se muestra el modelo básico.
Imports System.Threading.Tasks
Imports System.Collections.Concurrent
Module PartitionDemo
Sub Main()
' Source must be array or IList.
Dim source = Enumerable.Range(0, 100000).ToArray()
' Partition the entire source array.
' Let the partitioner size the ranges.
Dim rangePartitioner = Partitioner.Create(0, source.Length)
Dim results(source.Length - 1) As Double
' Loop over the partitions in parallel. The Sub is invoked once per partition.
Parallel.ForEach(rangePartitioner, Sub(range, loopState)
' Loop over each range element without a delegate invocation.
For i As Integer = range.Item1 To range.Item2 - 1
results(i) = source(i) * Math.PI
Next
End Sub)
Console.WriteLine("Operation complete. Print results? y/n")
Dim input As Char = Console.ReadKey().KeyChar
If input = "y"c Or input = "Y"c Then
For Each d As Double In results
Console.Write("{0} ", d)
Next
End If
End Sub
End Module
Cada subproceso del bucle recibe su propio Tuple(Of T1, T2) que contiene los valores de índice de
inicio y de fin del subintervalo especificado. El bucle for interno utiliza los valores toExclusive y
fromInclusive para recorrer directamente la matriz o IList.
Una de las sobrecargas Create permite especificar el tamaño y el número de las particiones. Esta
sobrecarga se puede utilizar en escenarios donde el trabajo por elemento es tan bajo que incluso una
llamada al método virtual por elemento tiene un impacto notable en el rendimiento.
Particionadores personalizados
En algunos escenarios, valdría la pena o incluso podría ser preciso implementar un particionador
propio. Por ejemplo, podría tener una clase de colección personalizada que puede crear particiones
más eficazmente que los particionadores predeterminados, basándose en su conocimiento de la
estructura interna de la clase. O tal vez desee crear particiones por intervalos de tamaños diferentes
basándose en su conocimiento de cuánto tiempo tardará en procesar los elementos en ubicaciones
diferentes de la colección de origen.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 97 de 107
Para crear un particionador personalizado básico, derive una clase de
System.Collections.Concurrent.Partitioner(Of TSource) e invalide los métodos virtuales, tal y como se
describe en la siguiente tabla.
GetPartitions
El subproceso principal llama a este método una vez y devuelve
IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle o la
consulta puede llamar a GetEnumerator en la lista para recuperar
IEnumerator(Of T) de una partición distinta.
SupportsDynamicPartitions Devuelve true si implementa GetDynamicPartitions, de lo contrario, false.
GetDynamicPartitions
Si SupportsDynamicPartitions es true, se puede llamar a este método
opcionalmente en lugar de a GetPartitions.
Si los resultados deben ser ordenables o si necesita acceso indizado a los elementos, derive de
System.Collections.Concurrent.OrderablePartitioner(Of TSource) e invalide sus métodos virtuales tal y
como se describe en la siguiente tabla.
GetPartitions
El subproceso principal llama a este método una vez y devuelve
IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle o
la consulta puede llamar a GetEnumerator en la lista para recuperar
IEnumerator(Of T) de una partición distinta.
SupportsDynamicPartitions
Devuelve true si implementa GetDynamicPartitions; de lo contrario,
falso.
GetDynamicPartitions Normalmente, solo llama a GetOrderableDynamicPartitions.
GetOrderableDynamicPartitions
Si SupportsDynamicPartitions es true, se puede llamar a este método
opcionalmente en lugar de a GetPartitions.
En la siguiente tabla se proporcionan los detalles adicionales sobre cómo los tres tipos de
particionadores del equilibrio de carga implementan la clase OrderablePartitioner(Of TSource).
Propiedad o método
IList / matriz
sin equilibrio
de carga
IList / matriz con
equilibrio de carga
IEnumerable
GetOrderablePartitions
Utiliza la
creación de
particiones
por intervalos
Utiliza la creación de
particiones por
fragmentos
optimizada para la
partitionCount
especificada
Utiliza la creación de
particiones por
fragmentos y crea
un número de
particiones estáticas.
OrderablePartitioner(Of
TSource).GetOrderableDynamicPartitions
Produce una
excepción no
admitida
Utiliza la creación de
particiones por
fragmentos
optimizada para las
listas y las particiones
dinámicas
Utiliza la creación de
particiones por
fragmentos creando
un número de
particiones
dinámico.
KeysOrderedInEachPartition Devuelve true Devuelve true Devuelve true
KeysOrderedAcrossPartitions Devuelve true Devuelve false Devuelve false
KeysNormalized Devuelve true Devuelve true Devuelve true
SupportsDynamicPartitions
Devuelve
false
Devuelve true Devuelve true
Particiones dinámicas
Si piensa utilizar el particionador en un método ForEach, debe poder devolver un número de
particiones dinámico. Esto significa que el particionador pueden proporcionar un enumerador para
una nueva partición a petición en cualquier momento durante la ejecución del bucle. Básicamente,
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 98 de 107
cada vez que el bucle agrega una nueva tarea en paralelo, solicita una nueva partición para esa tarea.
Si exige que los datos se puedan ordenar, derive de
System.Collections.Concurrent.OrderablePartitioner(Of TSource) para que cada elemento de cada
partición tenga asignado un índice único.
Contrato para particionadores
Cuando implemente un particionador personalizado, siga estas instrucciones para asegurarse de que
la interacción es correcta con PLINQ y ForEach en TPL:
Si se llama a GetPartitions con un argumento de cero o menos para partitionsCount, se
produce una ArgumentOutOfRangeException. Aunque PLINQ y TPL nunca pasarán en una
partitionCount igual a 0, recomendamos, no obstante, que se proteja ante esa posibilidad.
GetPartitions y GetOrderablePartitions siempre deberían devolver el número de particiones
partitionsCount. Si particionador se ejecuta fuera de los datos y no puede crear tantas
particiones como se solicitan, el método debería devolver un enumerador vacío para cada
una de las particiones restantes. De lo contrario, PLINQ y TPL producirán una
InvalidOperationException.
GetPartitions, GetOrderablePartitions, GetDynamicPartitions y
GetOrderableDynamicPartitions nunca deberían devolver null (Nothing en Visual Basic). Si lo
hacen, PLINQ / TPL producirán una excepción InvalidOperationException.
Los métodos que devuelven particiones siempre deberían devolver particiones que puedan
enumerar completamente y de forma única el origen de datos. No debería haber ninguna
duplicación en el origen de datos ni elementos omitidos a menos que lo requiera
específicamente el particionador. Si no se sigue esta regla, se puede alterar el orden del
resultado.
Los siguientes captadores get booleanos siempre deben devolver con precisión los
siguientes valores para que no se altere el orden de salida:
o KeysOrderedInEachPartition: cada partición devuelve los elementos con índices de
clave en aumento.
o KeysOrderedAcrossPartitions: para todas las particiones que se devuelven, los
índices de la clave de la partición i son más altos que los índices de la clave en la
partición i-1.
o KeysNormalized: todos los índices de clave aumentan consecutivamente,
comenzando por cero.
Todos los índices deben ser únicos. No puede haber índices duplicados. Si no se sigue esta
regla, se puede alterar el orden del resultado.
Todos los índices deben ser no negativos. Si no se sigue esta regla, PLINQ/TPL pueden
producir excepciones.
5.1. Cómo: Implementar las particiones dinámicas
En el siguiente ejemplo se muestra cómo implementar un System.Collections.Concurrent.Orderable
Partitioner(Of TSource) personalizado que implementa la creación de particiones dinámicas y que se
puede utilizar desde algunas sobrecargas de ForEach y de PLINQ.
Ejemplo
Cada vez que una partición llama a MoveNext en el enumerador, éste proporciona un elemento de
lista a la partición. En el caso de PLINQ y ForEach, la partición es una instancia de Task. Dado que las
solicitudes se producen simultáneamente en varios subprocesos, se sincroniza el acceso al índice
actual.
Imports System.Threading
Imports System.Threading.Tasks
Imports System.Collections.Concurrent
Module Module1
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 99 de 107
Public Class OrderableListPartitioner(Of TSource)
Inherits OrderablePartitioner(Of TSource)
Private ReadOnly m_input As IList(Of TSource)
Public Sub New(ByVal input As IList(Of TSource))
MyBase.New(True, False, True)
m_input = input
End Sub
' Must override to return true.
Public Overrides ReadOnly Property SupportsDynamicPartitions As Boolean
Get
Return True
End Get
End Property
Public Overrides Function GetOrderablePartitions(ByVal partitionCount As Integer) As IList(Of IEnumerator(Of
KeyValuePair(Of Long, TSource)))
Dim dynamicPartitions = GetOrderableDynamicPartitions()
Dim partitions(partitionCount - 1) As IEnumerator(Of KeyValuePair(Of Long, TSource))
For i = 0 To partitionCount - 1
partitions(i) = dynamicPartitions.GetEnumerator()
Next
Return partitions
End Function
Public Overrides Function GetOrderableDynamicPartitions() As IEnumerable(Of KeyValuePair(Of Long, TSource))
Return New ListDynamicPartitions(m_input)
End Function
Private Class ListDynamicPartitions
Implements IEnumerable(Of KeyValuePair(Of Long, TSource))
Private m_input As IList(Of TSource)
Friend Sub New(ByVal input As IList(Of TSource))
m_input = input
End Sub
Public Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of Long, TSource)) Implements
IEnumerable(Of KeyValuePair(Of Long, TSource)).GetEnumerator
Return New ListDynamicPartitionsEnumerator(m_input)
End Function
Public Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return CType(Me, IEnumerable).GetEnumerator()
End Function
End Class
Private Class ListDynamicPartitionsEnumerator
Implements IEnumerator(Of KeyValuePair(Of Long, TSource))
Private m_input As IList(Of TSource)
Shared m_pos As Integer = 0
Private m_current As KeyValuePair(Of Long, TSource)
Public Sub New(ByVal input As IList(Of TSource))
m_input = input
m_pos = 0
Me.disposedValue = False
End Sub
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 100 de 107
Public ReadOnly Property Current As KeyValuePair(Of Long, TSource) Implements IEnumerator(Of
KeyValuePair(Of Long, TSource)).Current
Get
Return m_current
End Get
End Property
Public ReadOnly Property Current1 As Object Implements IEnumerator.Current
Get
Return Me.Current
End Get
End Property
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Dim elemIndex = Interlocked.Increment(m_pos) - 1
If elemIndex >= m_input.Count Then
Return False
End If
m_current = New KeyValuePair(Of Long, TSource)(elemIndex, m_input(elemIndex))
Return True
End Function
Public Sub Reset() Implements IEnumerator.Reset
m_pos = 0
End Sub
Private disposedValue As Boolean ' To detect redundant calls
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
If Not Me.disposedValue Then
m_input = Nothing
m_current = Nothing
End If
Me.disposedValue = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
End Class
End Class
Class ConsumerClass
Shared Sub Main()
Console.BufferHeight = 20000
Dim nums = Enumerable.Range(0, 2000).ToArray()
Dim partitioner = New OrderableListPartitioner(Of Integer)(nums)
' Use with Parallel.ForEach
Parallel.ForEach(partitioner, Sub(i) Console.Write("{0}:{1} ", i, Thread.CurrentThread.ManagedThreadId))
Console.WriteLine("PLINQ -----------------------------------")
' create a new partitioner, since Enumerators are not reusable.
Dim partitioner2 = New OrderableListPartitioner(Of Integer)(nums)
' Use with PLINQ
Dim query = From num In partitioner2.AsParallel()
Where num Mod 8 = 0
Select num
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 101 de 107
For Each v In query
Console.Write("{0} ", v)
Next
Console.WriteLine("press any key")
Console.ReadKey()
End Sub
End Class
End Module
Éste es un ejemplo de creación de particiones de fragmentos, y cada fragmento se compone de un
elemento. Proporcionando más elementos a la vez, podría reducir la contención sobre el bloqueo y
teóricamente lograr un rendimiento más rápido. Sin embargo, en algún punto, los fragmentos
mayores podrían requerir lógica de equilibrio de carga adicional para mantener todos los
subprocesos ocupados hasta que se finalice todo el trabajo.

5.2. Cómo: Implementar un particionador con un número estático de
particiones
En el siguiente ejemplo se muestra una manera de implementar un particionador personalizado
simple para PLINQ que realiza la creación de particiones estáticas. Dado que el particionador no
admite las particiones dinámicas, no se puede usar de Parallel.ForEach. Este particionador
determinado podría proporcionar más velocidad que el particionador del intervalo predeterminado
para los orígenes de datos en los que cada elemento requiere una cantidad creciente de tiempo de
proceso.
Las particiones de este ejemplo están basadas en la hipótesis de un aumento lineal del tiempo de
proceso por cada elemento. En la práctica, podría ser difícil predecir los tiempos de proceso de esta
manera. Si está utilizando un particionador estático con un origen de datos concreto, puede
optimizar la fórmula de creación de particiones del origen, agregar lógica de equilibrio de carga o
emplear un enfoque de creación de particiones de los fragmentos, como se muestra en Cómo:
Implementar las particiones dinámicas.

6. Generadores de tareas
Un generador de tareas se representa mediante la clase System.Threading.Tasks.TaskFactory, que
crea objetos Task, o la clase System.Threading.Tasks.TaskFactory(Of TResult), que crea objetos
Task(Of TResult). Ambas clases contienen métodos que puede utilizar para:
Crear tareas e iniciarlas inmediatamente.
Crear continuaciones de tareas que se inicien cuando alguna o toda una matriz de tareas se
complete.
Crear tareas que representen pares de métodos de comienzo/fin que siguen el Modelo de
programación asincrónica.
La clase Task tiene una propiedad estática que representa TaskFactory predeterminada.
Normalmente, los métodos TaskFactory se invocan utilizando la propiedad Factory, como se muestra
en el siguiente ejemplo.
Dim taskA as Task = Task.Factory.StartNew(Sub( ...))
En la mayoría de los casos, no tiene que derivar una nueva clase de TaskFactory. Sin embargo, a veces
es útil configurar una nueva TaskFactory y utilizarla para especificar algunas opciones o asociar tareas
a un programador personalizado. En el siguiente ejemplo se muestra cómo configurar una nueva
TaskFactory que crea tareas que usan el TaskScheduler especificado y tiene las opciones
TaskCreationOptions especificadas.
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 102 de 107
Class Program
Shared Sub Main()
Dim cts As CancellationTokenSource = New CancellationTokenSource()
Dim _factory As TaskFactory = New TaskFactory(
cts.Token,
TaskCreationOptions.PreferFairness,
TaskContinuationOptions.ExecuteSynchronously,
New MyScheduler())
Dim t2 = _factory.StartNew(Sub() DoWork())
End Sub
Shared Sub DoWork()
End Sub

7. Programadores de tareas
Los programadores de tarea se representan mediante la clase System.Threading.Tasks.TaskScheduler.
Un programador de tareas se asegura de que se ejecuta el trabajo de una tarea. El programador de
tareas predeterminado está basado en .NET Framework 4 ThreadPool, que proporciona robo de
trabajo para el equilibrio de carga, inyección/retirada de subprocesos para obtener el máximo
resultado y un buen rendimiento en general. Debería ser suficiente para la mayoría de los escenarios.
Sin embargo, si necesita funcionalidad especial, puede crear un programador personalizado y
habilitarlo para tareas o consultas concretas.
Programador de tareas predeterminado y ThreadPool
El programador predeterminado para Task Parallel Library y PLINQ utiliza .NET Framework
ThreadPool para poner en cola y ejecutar el trabajo. En .NET Framework 4, ThreadPool utiliza la
información que proporciona el tipo System.Threading.Tasks.Task para admitir el paralelismo
específico (unidades efímeras de trabajo) que las tareas y consultas paralelas representan a menudo.
Cola global ThreadPool frente acolas locales
Como en versiones anteriores de .NET Framework, ThreadPool mantiene una cola de trabajo FIFO
(primero en llegar, primero en salir) global para los subprocesos en cada dominio de aplicación.
Cuando un programa llama a QueueUserWorkItem (o UnsafeQueueUserWorkItem), el trabajo se
coloca en esta cola compartida y finalmente sale de la cola hacia el subproceso siguiente que está
disponible. En .NET Framework 4, esta cola se ha mejorado para utilizar un algoritmo sin bloqueo que
se parece la clase ConcurrentQueue. utilizando esta implementación sin bloqueo, ThreadPool gasta
menos horario cuando pone en la cola y los elementos de trabajo de cola. Esta ventaja de
rendimiento está disponible para todos los programas que utilizan ThreadPool.
Las tareas de nivel superior, que son tareas que no se crean en el contexto de otra tarea, se colocan
en la cola global igual que cualquier otro elemento de trabajo. Sin embargo, las tareas anidadas o
secundarias, que se crean en el contexto de otra tarea, se controlan de forma bastante distinta. Una
tarea secundaria o anidada se coloca en una cola local que es específica del subproceso en el que la
tarea primaria se está ejecutando. La tarea primaria puede ser una tarea de nivel superior o también
puede ser el elemento secundario de otra tarea. Cuando este subproceso está listo para más trabajo,
primero busca en la cola local. Si hay elementos de trabajo esperando, se puede tener acceso a ellos
rápidamente. Se tiene acceso a las colas locales en el orden último en entrar (LIFO), primero en salir
con el fin de conservar la situación de la memoria caché y reducir la contención.
En el siguiente ejemplo se muestran algunas tareas que se programan en la cola global y otras que se
programan en la cola local.
Sub QueueTasks()
' TaskA is a top level task.
Dim taskA = Task.Factory.StartNew(Sub()
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 103 de 107
Console.WriteLine("I was enqueued on the thread pool's global queue.")
' TaskB is a nested task and TaskC is a child task. Both go to local queue.
Dim taskB = New Task(Sub() Console.WriteLine("I was enqueued on the local queue."))
Dim taskC = New Task(Sub() Console.WriteLine("I was enqueued on the local queue, too."),
TaskCreationOptions.AttachedToParent)
taskB.Start()
taskC.Start()
End Sub)
End Sub
El uso de colas locales reduce no solo la presión en la cola global, también aprovecha la situación de
los datos. Los elementos de trabajo de la cola local con frecuencia hacen referencia a estructuras de
datos que están físicamente cerca unos de otros en memoria. En estos casos, los datos ya están en la
memoria caché después de que la primera tarea se haya ejecutado, y se puede obtener acceso
rápidamente. LINQ Paralelo (PLINQ) y la clase Parallel usa tareas anidadas y tareas secundarias
extensivamente y logran aumentos significativos de velocidad utilizando las colas de trabajo locales.
Robo de trabajo
ThreadPool también representa un algoritmo de robo de trabajo para ayudar a asegurar que ningún
subproceso esté inactivo mientras otros todavía tienen trabajo en sus colas. Cuando un subproceso
ThreadPool está listo para más trabajo, examina primero el encabezado de la cola local, a
continuación, en la cola global y después en las colas locales de otros subprocesos. Si encuentra un
elemento de trabajo en la cola local de otro subproceso, aplica primero heurística para asegurarse de
que puede ejecutar el trabajo eficazmente. Si puede, quita el elemento de trabajo de la cola (en
orden FIFO). Esto reduce la contención en cada cola local y conserva la situación de los datos. Esta
arquitectura ayuda el equilibrio de la carga trabajo más eficazmente que las versiones pasadas
hicieron.
Tareas de ejecución prolongada
Tal vez le interese evitar explícitamente que una tarea se coloque en una cola local. Por ejemplo,
puede saber que un elemento de trabajo determinado se ejecutará durante un tiempo relativamente
largo y es probable que bloquee el resto de los elementos de trabajo de la cola local. En este caso,
puede especificar la opción LongRunning, que proporciona una sugerencia al programador que le
indica que tal vez es necesario un subproceso adicional para que la tarea no bloquee el progreso de
otros subprocesos o elementos de trabajo de la cola local. Utilizando esta opción, se evita
ThreadPool completamente, incluidas las colas global y locales.
Inclusión de tareas
En algunos casos, cuando se espera un tarea, se puede ejecutar sincrónicamente en el subproceso
que está realizando la operación de espera. Esto mejora el rendimiento, porque evita la necesidad de
un subproceso adicional mediante el uso del subproceso existente que, de otro modo, se habría
bloqueado. Para evitar errores después de volver a entrar, la inclusión de tareas solo tiene lugar
cuando el destino de la espera se encuentra en la cola local del subproceso pertinente.
Especificar un contexto de sincronización
Puede utilizar el método TaskScheduler.FromCurrentSynchronizationContext para especificar que una
tarea se debería programar para ejecutarse en un subproceso determinado. Esto es útil en marcos
como Windows Forms y Windows Presentation Foundation, donde el acceso a los objetos de interfaz
de usuario está restringido a menudo para el código que se está ejecutando en el mismo subproceso
en el que se creó el objeto UI.

7.1. Cómo: Crear un programador de tareas que limita el grado de
simultaneidad
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 104 de 107
En algunos casos no muy usuales, podría lograr aumentar el rendimiento creando un programador
de tareas personalizado que se derive de la clase System.Threading.Tasks.TaskScheduler. Después
podría especificar este programador en un método For o ForEach utilizando la enumeración
System.Threading.Tasks.ParallelOptions. Al utilizar los objetos Task directamente, puede especificar el
programador personalizado mediante el constructor TaskFactory que toma TaskScheduler como un
parámetro de entrada o por algún otro medio como StartNew.
También puede utilizar un programador personalizado para lograr la funcionalidad que el
programador predeterminado no proporciona, como es el orden de ejecución estricto de primero en
entrar, primero en salir (FIFO). En el ejemplo siguiente se muestra cómo crear un programador de
tareas personalizado. Este programador permite especificar el grado de simultaneidad.
Ejemplo
El siguiente ejemplo procede de Parallel Extensions Samples del sitio web Galería de código de
MSDN.
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Threading
Imports System.Threading.Tasks
Module Module2
Sub Main()
' Create a scheduler that uses only one thread.
Dim lcts As New LimitedConcurrencyLevelTaskScheduler(1)
' Create a TaskFactory and pass it our custom scheduler.
Dim factory As New TaskFactory(lcts)
Dim cts As New CancellationTokenSource()
' Use our factory to run a task.
Dim t As Task = factory.StartNew(Sub()
For i As Integer = 1 To 50000
Console.Write("{0} on thread {1}. ", i, Thread.CurrentThread.ManagedThreadId)
Next
End Sub,
cts.Token)
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
''' <summary>
''' Provides a task scheduler that ensures a maximum concurrency level While running on top of the ThreadPool.
''' </summary>
Public Class LimitedConcurrencyLevelTaskScheduler
Inherits TaskScheduler
''' <summary>Whether the current thread is processing work items.</summary>
<ThreadStatic()>
Private Shared _currentThreadIsProcessingItems As Boolean
''' <summary>The list of tasks to be executed.</summary>
Private ReadOnly _tasks As LinkedList(Of Task) = New LinkedList(Of Task)() ' protected by lock(_tasks)
''' <summary>The maximum concurrency level allowed by this scheduler.</summary>
Private ReadOnly _maxDegreeOfParallelism As Integer
''' <summary>Whether the scheduler is currently processing work items.</summary>
Private _delegatesQueuedOrRunning As Integer = 0 ' protected by lock(_tasks)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 105 de 107
''' <summary>
''' Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the specified degree of
parallelism.
''' </summary>
''' <param name="maxDegreeOfParallelism">The maximum degree of parallelism provided by this
scheduler.</param>
Public Sub New(ByVal maxDegreeOfParallelism As Integer)
If (maxDegreeOfParallelism < 1) Then
Throw New ArgumentOutOfRangeException("maxDegreeOfParallelism")
End If
_maxDegreeOfParallelism = maxDegreeOfParallelism
End Sub
''' <summary>Queues a task to the scheduler.</summary>
''' <param name="t">The task to be queued.</param>
Protected Overrides Sub QueueTask(ByVal t As Task)
' Add the task to the list of tasks to be processed. If there aren't enough
' delegates currently queued or running to process tasks, schedule another.
SyncLock (_tasks)
_tasks.AddLast(t)
If (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) Then
_delegatesQueuedOrRunning = _delegatesQueuedOrRunning + 1
NotifyThreadPoolOfPendingWork()
End If
End SyncLock
End Sub
''' <summary>
''' Informs the ThreadPool that there's work to be executed for this scheduler.
''' </summary>
Private Sub NotifyThreadPoolOfPendingWork()
ThreadPool.UnsafeQueueUserWorkItem(Sub()
' Note that the current thread is now processing work items.
' This is necessary to enable inlining of tasks into this thread.
_currentThreadIsProcessingItems = True
Try
' Process all available items in the queue.
While (True)
Dim item As Task
SyncLock (_tasks)
' When there are no more items to be processed,
' note that we're done processing, and get out.
If (_tasks.Count = 0) Then
_delegatesQueuedOrRunning = _delegatesQueuedOrRunning - 1
Exit While
End If
' Get the next item from the queue
item = _tasks.First.Value
_tasks.RemoveFirst()
End SyncLock
' Execute the task we pulled out of the queue
MyBase.TryExecuteTask(item)
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 106 de 107
End While
' We're done processing items on the current thread
Finally
_currentThreadIsProcessingItems = False
End Try
End Sub,
Nothing)
End Sub
''' <summary>Attempts to execute the specified task on the current thread.</summary>
''' <param name="task">The task to be executed.</param>
''' <param name="taskWasPreviouslyQueued"></param>
''' <returns>Whether the task could be executed on the current thread.</returns>
Protected Overrides Function TryExecuteTaskInline(ByVal t As Task, ByVal taskWasPreviouslyQueued As
Boolean) As Boolean
' If this thread isn't already processing a task, we don't support inlining
If (Not _currentThreadIsProcessingItems) Then
Return False
End If
' If the task was previously queued, remove it from the queue
If (taskWasPreviouslyQueued) Then
TryDequeue(t)
End If
' Try to run the task.
Return MyBase.TryExecuteTask(t)
End Function
''' <summary>Attempts to remove a previously scheduled task from the scheduler.</summary>
''' <param name="t">The task to be removed.</param>
''' <returns>Whether the task could be found and removed.</returns>
Protected Overrides Function TryDequeue(ByVal t As Task) As Boolean
SyncLock (_tasks)
Return _tasks.Remove(t)
End SyncLock
End Function
''' <summary>Gets the maximum concurrency level supported by this scheduler.</summary>
Public Overrides ReadOnly Property MaximumConcurrencyLevel As Integer
Get
Return _maxDegreeOfParallelism
End Get
End Property
''' <summary>Gets an enumerable of the tasks currently scheduled on this scheduler.</summary>
''' <returns>An enumerable of the tasks currently scheduled.</returns>
Protected Overrides Function GetScheduledTasks() As IEnumerable(Of Task)
Dim lockTaken As Boolean = False
Try
Monitor.TryEnter(_tasks, lockTaken)
If (lockTaken) Then
Return _tasks.ToArray()
Else
Throw New NotSupportedException()
End If
Programación Paralela en .NET Framework
MCT: Luis Dueñas Pag 107 de 107
Finally
If (lockTaken) Then
Monitor.Exit(_tasks)
End If
End Try
End Function
End Class
End Module