Beruflich Dokumente
Kultur Dokumente
NET
Framework 4.5
Manual de .NET Framework 4.5
1. Subprocesamiento administrado
1.1. Principios básicos del subprocesamiento administrado
1.1.1.Subprocesos y subprocesamiento
1.1.2.Excepciones en subprocesos administrados
1.1.3.Sincronizar datos para subprocesamiento múltiple
1.1.4.Estados de subprocesos administrados
1.1.5.Subprocesos de primer y segundo plano
1.1.6.Subprocesamiento administrado y no administrado en Windows
1.1.7.Thread.Suspend, recolección de elementos no utilizados y puntos de seguridad
1.1.8.Almacenamiento local de subprocesos: Campos estáticos relacionados con
subprocesos y ranuras de datos
1.1.9.Cancelación en subprocesos administrados
1.1.9.1. Cómo: Realizar escuchas de solicitudes mediante sondeo
1.1.9.2. Cómo: Registrar devoluciones de llamadas de solicitudes de
cancelación
1.1.9.3. Cómo: Realizar escuchas de solicitudes de cancelación cuando tienen
controladores de espera
1.1.9.4. Cómo: Realizar escuchas de varias solicitudes de cancelación
1.2. Utilizar subprocesos y subprocesamiento
1.2.1.Crear subprocesos y analizar los datos en el inicio
1.2.2.Pausar y reanudar subprocesos
1.2.3.Destruir subprocesos
1.2.4.Planear subprocesos
1.2.5.Cancelar subprocesos de manera cooperativa
1.3. Procedimientos recomendados para el subprocesamiento administrado
1.4. Objetos y características de subprocesos
1.4.1.Grupo de subprocesos administrados
1.4.2.Temporizadores
1.4.3.Monitores
1.4.4.Controladores de espera
1.4.5.Controladores de espera
1.4.5.1. EventWaitHandle
1.4.5.2. AutoResetEvent
1.4.5.3. ManualResetEvent y ManualResetEventSlim
1.4.5.4. CountdownEvent
1.4.6.Exclusiones mutuas (mutex)
1.4.7.Operaciones de bloqueo
1.4.8.Bloqueos de lector y escritor
1.4.9.Semaphore y SemaphoreSlim
1.4.10. Información general sobre los primitivos de sincronización
1.4.11. Barrier
1.4.11.1. Cómo: Sincronizar operaciones simultáneas con una clase Barrier
1.4.12. SpinLock
Comunicarse a través de una red, con un servidor Web y una base de datos.
Realizar operaciones que requieren una gran cantidad de tiempo.
Distinguir tareas de diversas prioridades. Por ejemplo, un subproceso de prioridad alta
administra tareas en las que el tiempo es decisivo y un proceso de prioridad baja realiza
otras tareas.
Permitir a la interfaz de usuario que siga respondiendo, mientras se asigna tiempo a las
tareas en segundo plano.
El sistema utiliza memoria para la información de contexto requerida por los procesos,
objetos AppDomain y subprocesos. Por tanto, el número de procesos, objetos
AppDomain y subprocesos que se pueden crear está limitado por la memoria
disponible.
Si se proporciona acceso compartido a los recursos, se pueden producir conflictos. Para evitar
los conflictos, es necesario sincronizar o controlar el acceso a los recursos compartidos. Si no se
sincroniza el acceso correctamente (en los mismos dominios de aplicación o en otros) se pueden
producir problemas como interbloqueos (en los que dos subprocesos dejan de responder
mientras cada uno espera a que el otro se complete) y condiciones de carrera (cuando se produce
un resultado anómalo debido a una dependencia decisiva e inesperada de la duración de dos
eventos). El sistema proporciona objetos de sincronización que pueden utilizarse para coordinar
recursos compartidos entre varios subprocesos. Si se reduce el número de subprocesos, se
facilita la sincronización de los recursos.
Entre los recursos que requieren sincronización se incluyen los siguientes:
Recursos del sistema (como puertos de comunicaciones).
Recursos compartidos por varios procesadores (como identificadores de archivo).
Los recursos de un único dominio de aplicación (como campos globales, estáticos y de
instancia) a los que tienen acceso varios subprocesos.
Subprocesamiento y diseño de aplicaciones
En general, el uso de la clase ThreadPool es la forma más fácil de controlar varios subprocesos
para tareas relativamente cortas que no bloquearán otros subprocesos y cuando no se espera
ninguna programación particular de las tareas. No obstante, hay diversos motivos para crear sus
propios subprocesos:
Si es necesario que una tarea tenga una prioridad en particular.
Si tiene una tarea que podría ejecutarse durante bastante tiempo (y, por tanto, bloquear
otras tareas).
Si debe ubicar subprocesos en un contenedor uniproceso (todos los subprocesos
ThreadPool se encuentran en un contenedor multiproceso).
Si necesita asociar una identidad estable al subproceso. Por ejemplo, debería utilizar un
subproceso dedicado para anular dicho subproceso, suspenderlo o detectarlo por
nombre.
Si es necesario ejecutar subprocesos de fondo que interactúan con la interfaz de usuario,
la versión 2.0 de .NET Framework proporciona un componente BackgroundWorker que
se comunica utilizando eventos, con cálculo de referencias entre subprocesos al
subproceso de la interfaz de usuario.
Subprocesamiento y excepciones
Controle las excepciones en subprocesos. Las excepciones no controladas en subprocesos,
incluso los subprocesos de fondo, generalmente finalizan el proceso. Hay tres excepciones a esta
regla:
Nota
En las versiones 1.0 y 1.1 de .NET Framework, Common Language Runtime intercepta
silenciosamente algunas excepciones, como por ejemplo en subprocesos ThreadPool. Esto
puede dañar el estado de la aplicación y hacer que en el futuro las aplicaciones no respondan, lo
que podría ser muy difícil de depurar.
Reemplazo de host
En .NET Framework versión 2.0, un host no administrado puede utilizar la interfaz
ICLRPolicyManager de la API de hospedaje para invalidar la directiva predeterminada de
excepciones no controladas de Common Language Runtime. La función
ICLRPolicyManager::SetUnhandledExceptionPolicy se utiliza para establecer la directiva para
las excepciones no controladas.
1.1.3. Sincronizar datos para subprocesamiento múltiple
Cuando varios subprocesos pueden llamar a las propiedades y métodos de un solo objeto, es
fundamental sincronizar las llamadas. En caso contrario, un subproceso puede interrumpir la
ejecución de otro subproceso, y el objeto puede terminar teniendo un estado no válido. Se dice
que una clase es segura para subprocesos cuando sus miembros están protegidos contra este tipo
de interrupciones.
La infraestructura de Common Language proporciona diversas estrategias para sincronizar el
acceso a instancias y miembros estáticos:
Sin sincronización
Ésta es la opción predeterminada para objetos. Cualquier subproceso puede tener acceso a un
método o campo en cualquier momento. Sólo debería tener acceso a estos objetos un subproceso
cada vez.
Sincronización manual
La biblioteca de clases de .NET Framework proporciona una serie de clases para sincronizar los
subprocesos.
Regiones de código sincronizado
Puede utilizar la clase Monitor o una palabra clave del compilador para sincronizar bloques de
código, métodos de instancia y métodos estáticos. No se admiten los campos estáticos
sincronizados.
Visual Basic y C# admiten el marcado de bloques de código con una determinada palabra clave
de lenguaje, la instrucción lock en C# o la instrucción SyncLock en Visual Basic. Cuando un
subproceso ejecuta el código, se realiza un intento de bloqueo. Si otro subproceso ha realizado
ya el bloqueo, el subproceso se bloquea hasta que esté disponible el bloqueo. Cuando el
subproceso sale del bloque sincronizado de código, el bloqueo se libera, independientemente de
cómo salga del bloque el subproceso.
Nota
Las instrucciones lock y SyncLock se implementan mediante Monitor.Enter y Monitor.Exit, por
lo que se pueden utilizar otros métodos de Monitor con ellas en el área sincronizada.
También puede decorar un método con MethodImplAttribute y
MethodImplOptions.Synchronized, lo que tiene el mismo efecto que utilizar Monitor o una
de las palabras clave del compilador para bloquear todo el cuerpo del método.
Se puede utilizar Thread.Interrupt para que un subproceso salga de operaciones de bloqueo, por
ejemplo, de la espera para tener acceso a una región de código sincronizado. También se utiliza
Thread.Interrupt para que los subprocesos salgan de operaciones como Thread.Sleep.
Importante
No bloquee el tipo, es decir, typeof(MyType) en C#, GetType(MyType) en Visual Basic o
MyType::typeid en C++, para proteger los métodos static (métodos Shared en Visual Basic).
Utilice en su lugar un objeto estático privado. De igual forma, no utilice this en C# (Me en
Visual Basic) para bloquear los métodos de instancia. Utilice en su lugar un objeto privado. Una
clase o instancia se puede bloquear por código distinto del suyo propio, produciendo potenciales
interbloqueos o problemas de rendimiento.
Compatibilidad del compilador
Visual Basic y C# admiten una palabra clave que utiliza Monitor.Enter y Monitor.Exit para
bloquear el objeto. Visual Basic admite la instrucción SyncLock y C# admite la instrucción
lock.
En ambos casos, si se inicia una excepción en el bloque de código, el bloqueo adquirido por
lock o SyncLock se libera automáticamente. Los compiladores de C# y de Visual Basic emiten
un bloque try/finally con Monitor.Enter al principio de try y Monitor.Exit en el bloque
finally. Si se inicia una excepción dentro del bloque lock o SyncLock, se ejecuta el controlador
finally para permitir realizar cualquier trabajo de limpieza.
Contexto sincronizado
Puede utilizar el atributo SynchronizationAttribute en cualquier objeto ContextBoundObject
para sincronizar todos los métodos y campos de instancia. Todos los objetos del mismo dominio
de contexto comparten el mismo bloqueo. Varios subprocesos pueden tener acceso a los
métodos y campos, pero sólo uno al mismo tiempo.
1.1.4. Estados de subprocesos administrados
La propiedad Thread.ThreadState proporciona una máscara de bits que indica el estado actual
del subproceso. Un subproceso está siempre en al menos uno de los estados posibles en la
enumeración ThreadState y puede estar en varios estados al mismo tiempo.
Importante
El estado de los subprocesos sólo es de interés en algunos escenarios de depuración. El código
nunca debe utilizar el estado de los subprocesos para sincronizar las actividades de los
subprocesos.
Al crear un subproceso administrado, este se encuentra en el estado Unstarted. El subproceso
permanece en el estado Unstarted hasta que el sistema operativo lo cambia al estado iniciado.
Llamar a Start, permite al sistema operativo saber que se puede iniciar el subproceso, pero no
cambia su estado.
Muchas veces, los subprocesos están en más de un estado en un momento dado. Por ejemplo, si
se bloquea un subproceso en una llamada a Monitor.Wait y otro subproceso llama a Abort en
ese mismo subproceso, este estará en los estados WaitSleepJoin y AbortRequested al mismo
tiempo. En este caso, tan pronto como el subproceso vuelva de la llamada a Wait o se
interrumpa, recibirá la excepción ThreadAbortException.
Una vez que un subproceso deja el estado Unstarted como resultado de una llamada a Start, no
puede volver nunca al estado Unstarted. Un subproceso no puede dejar nunca el estado Stopped.
1.1.5. Subprocesos de primer y segundo plano
Un subproceso administrado es un subproceso en segundo plano o un subproceso de primer
plano. Los subprocesos en segundo plano son idénticos a los subprocesos en primer plano con
una excepción: un subproceso en segundo plano no mantiene activo el entorno de ejecución
administrado. Una vez que todos los subprocesos de primer plano se han detenido en un proceso
administrado (donde el archivo .exe es un ensamblado administrado), el sistema detiene todos
los subprocesos en segundo plano y se cierra.
Nota
Cuando el tiempo de ejecución detiene un subproceso en segundo plano porque el proceso está
cerrándose, no se produce ninguna excepción en el subproceso. Sin embargo, cuando los
subprocesos se detienen porque el método AppDomain.Unload descarga el dominio de
aplicación, se produce una excepción ThreadAbortException tanto en los subprocesos en primer
plano como en los subprocesos segundo plano.
Utilice la propiedad Thread.IsBackground para determinar si un subproceso es un subproceso en
segundo plano o en primer plano, o para cambiar su estado. Un subproceso puede cambiarse a
segundo plano en cualquier momento estableciendo su propiedad IsBackground en true.
Importante
El estado de primer plano o segundo plano de un subproceso no afecta al resultado de una
excepción no controlada en el subproceso. En la versión 2.0 de .NET Framework, una
excepción no controlada tanto en subprocesos en primer plano como en subprocesos en segundo
plano da como resultado la finalización de la aplicación.
Los subprocesos que pertenecen al grupo de subprocesos administrados (es decir, los
subprocesos cuya propiedad IsThreadPoolThread es true) son subprocesos en segundo plano.
Todos los subprocesos que entran en el entorno de ejecución administrado desde código no
administrado se marcan como subprocesos en segundo plano. Todos los subprocesos generados
al crear e iniciar un nuevo objeto Thread son subprocesos en primer plano de forma
predeterminada.
Si utiliza un subproceso para controlar una actividad, como una conexión de socket, establezca
su propiedad IsBackground en true para que el subproceso no impida la finalización del proceso.
1.1.6. Subprocesamiento administrado y no administrado en Windows
La administración de todos los subprocesos se realiza mediante la clase Thread, incluso la de los
creados por Common Language Runtime o fuera del motor en tiempo de ejecución que entran
en el entorno administrado para ejecutar el código. El motor en tiempo de ejecución supervisa
todos los subprocesos de su proceso que han ejecutado código alguna vez dentro del entorno de
ejecución administrado. No realiza el seguimiento de otros subprocesos. Los subprocesos
pueden entrar en el entorno de ejecución administrado a través de la interoperabilidad COM
(puesto que el motor en tiempo de ejecución expone los objetos administrados como objetos
COM al universo no administrado), la función COM DllGetClassObject() y por invocación de
plataforma.
Cuando un subproceso no administrado entra en el motor en tiempo de ejecución, por ejemplo, a
través de un contenedor CCW, el sistema comprueba el almacenamiento local de los
subprocesos de dicho subproceso para buscar un objeto administrado interno Thread. Si
encuentra alguno, el motor en tiempo de ejecución ya está al corriente de este subproceso. No
obstante, si no puede encontrar ninguno, compila un nuevo objeto Thread y lo instala en el
almacenamiento local de subprocesos de dicho subproceso.
En el subprocesamiento administrado, Thread.GetHashCode es la identificación estable del
subproceso administrado. Mientras dure el subproceso, no colisionará con el valor de ningún
otro subproceso, independientemente del dominio de aplicación del que se obtenga este valor.
Nota
Un ThreadId del sistema operativo no tiene relación fija con un subproceso administrado,
puesto que un host no administrado puede controlar la relación entre los subprocesos
administrados y los no administrados. En concreto, un host sofisticado puede utilizar la API de
fibra para programar muchos subprocesos administrados con el mismo subproceso del sistema
operativo o para pasar un subproceso administrado entre diferentes subprocesos del sistema
operativo.
Correspondencia entre el subprocesamiento de Win32 y el subprocesamiento
administrado
En la tabla siguiente se asignan elementos del subprocesamiento de Win32 a su equivalente
aproximado del motor en tiempo de ejecución. Tenga en cuenta que esta correspondencia no
representa una funcionalidad idéntica. Por ejemplo, TerminateThread no ejecuta cláusulas
finally ni libera recursos, y no se puede evitar. No obstante, Thread.Abort ejecuta todo el código
para revertir, recupera todos los recursos y puede denegarse mediante ResetAbort. Lea
atentamente la documentación antes de realizar suposiciones acerca de la funcionalidad.
En Win32 En Common Language Runtime
CreateThread Combinación de Thread y ThreadStart
TerminateThread Thread.Abort
SuspendThread Thread.Suspend
ResumeThread Thread.Resume
Sleep Thread.Sleep
WaitForSingleObject en el controlador del subproceso Thread.Join
ExitThread Ningún equivalente
GetCurrentThread Thread.CurrentThread
SetThreadPriority Thread.Priority
Ningún equivalente Thread.Name
De igual forma, cuando un subproceso obtiene la misma ranura de datos con nombre en dos
dominios de aplicación diferentes, los datos del primer dominio de aplicación son
independientes de los datos del segundo dominio de aplicación.
Campos estáticos relacionados con subprocesos
Si sabe que un dato siempre es exclusivo de una combinación de subproceso y dominio de
aplicación, aplique el atributo ThreadStaticAttribute al campo estático. Utilice el campo igual
que cualquier otro campo estático. Los datos del campo son exclusivos de cada subproceso que
los utiliza.
Los campos estáticos relacionados con subprocesos proporcionan un rendimiento mejor que las
ranuras de datos y tienen la ventaja de que comprueban los tipos en tiempo de compilación.
Tenga en cuenta que cualquier código constructor de clases se ejecutará en el primer subproceso
del primer contexto que tenga acceso al campo. En todos los demás subprocesos o contextos del
mismo dominio de aplicación, los campos se inicializarán en null (Nothing en Visual Basic) si
son tipos de referencia, o en sus valores predeterminados si son tipos de valor. Por consiguiente,
no debe confiar en los constructores de clases para inicializar campos estáticos relacionados con
subprocesos. En su lugar, evite inicializar los campos estáticos relacionados con subprocesos y
asuma que se inicializan en null (Nothing) o en sus valores predeterminados.
Ranuras de datos
.NET Framework proporciona ranuras de datos dinámicos que son exclusivas de una
combinación de dominio de aplicación y subproceso. Hay dos tipos de ranuras de datos: con
nombre y sin nombre. Ambas se implementan mediante la estructura LocalDataStoreSlot.
Para crear una ranura de datos con nombre, utilice el método
Thread.AllocateNamedDataSlot o Thread.GetNamedDataSlot. Para obtener una referencia a
una ranura con nombre existente, pase su nombre al método GetNamedDataSlot.
Para crear una ranura de datos sin nombre, utilice el método Thread.AllocateDataSlot.
Para las ranuras con nombre y sin nombre, utilice los métodos Thread.SetData y
Thread.GetData para establecer y recuperar la información de la ranura. Éstos son métodos
estáticos que siempre representan los datos del subproceso que los está ejecutando.
Las ranuras con nombre pueden resultar cómodas, porque puede recuperar la ranura cuando lo
necesite pasando su nombre al método GetNamedDataSlot, en lugar de mantener una referencia
a una ranura sin nombre. Sin embargo, si otro componente usa el mismo nombre para su propio
almacenamiento relacionado con subprocesos y un subproceso ejecuta código tanto de su
componente como del otro, ambos componentes podrían dañar los datos del otro. (En este
escenario se supone que ambos componentes se están ejecutando en el mismo dominio de
aplicación y que no están diseñados para compartir los mismos datos.)
1.1.9. Cancelación en subprocesos administrados
.NET Framework 4 presenta un nuevo modelo unificado para la cancelación cooperativa de
operaciones asincrónicas o sincrónicas de ejecución prolongada. Este modelo se basa en un
objeto ligero denominado token de cancelación. El objeto que invoca una operación cancelable,
por ejemplo creando un nuevo subproceso o tarea, pasa el token a la operación. Esa operación
puede pasar a su vez copias del token a otras operaciones. En algún momento posterior, el
objeto que creó el token puede usarlo para solicitar que la operación deje de hacer lo que está
haciendo. Solo el objeto solicitante puede emitir la solicitud de cancelación y cada agente de
escucha es responsable de observar la solicitud y responder a ella de manera puntual. En la
siguiente ilustración se muestra la relación entre un origen de token y todas las copias de su
token.
Nota
En el ejemplo se usa el método QueueUserWorkItem para mostrar que el nuevo marco de
cancelación es compatible con las API heredadas.
static void CancelWithThreadPoolMiniSnippet()
{
//Thread 1: The Requestor
// Create the token source.
CancellationTokenSource cts = new CancellationTokenSource();
// Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
// Request cancellation by setting a flag on the token.
cts.Cancel();
}
Si un objeto admite más de una operación cancelable simultánea, pase un token diferente como
entrada a cada operación cancelable distinta. De esa forma, se puede cancelar una operación sin
afectar a las demás.
Realizar escuchas y responder a solicitudes de cancelación
El implementador de una operación cancelable determina, en el delegado de usuario, cómo
finalizar la operación en respuesta a una solicitud de cancelación. En muchos casos, el delegado
de usuario puede realizar simplemente cualquier limpieza necesaria y volver inmediatamente.
Sin embargo, en casos más complejos podría ser necesario que un delegado de usuario notificara
al código de biblioteca que se ha producido la cancelación. En estos casos, la manera correcta de
finalizar la operación es que el delegado llame a ThrowIfCancellationRequested, que producirá
OperationCanceled Exception. Las nuevas sobrecargas de esta excepción en .NET Framework 4
toman CancellationToken como argumento. El código de biblioteca puede detectar esta
excepción en el subproceso de delegado de usuario y examinar el token de la excepción para
determinar si la excepción indica una cancelación cooperativa o alguna otra situación
excepcional.
La clase Task controla OperationCanceledException de esta forma.
Realizar escuchas mediante sondeo
En el caso de cálculos de ejecución prolongada que usan bucles o recorridos, puede escuchar
una solicitud de cancelación sondeando periódicamente el valor de la propiedad
CancellationToken. IsCancellationRequested. Si el valor es true, el método debe limpiar y
finalizar lo más rápidamente posible. La frecuencia óptima de sondeo depende del tipo de
aplicación. Es el desarrollador quien determina la mejor frecuencia de sondeo para cualquier
programa dado. El sondeo propiamente dicho no afecta al rendimiento considerablemente. En el
ejemplo siguiente se muestra una forma posible de sondeo.
static void NestedLoops(Rectangle rect, CancellationToken token)
{
for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++)
{
for (int y = 0; y < rect.rows; y++)
{
// Simulating work.
Thread.SpinWait(5000);
Console.Write("{0},{1} ", x, y);
}
// Assume that we know that the inner loop is very fast.
// Therefore, checking once per row is sufficient.
if (token.IsCancellationRequested)
{
// Cleanup or undo here if necessary...
Console.WriteLine("\r\nCancelling after row {0}.", x);
Console.WriteLine("Press any key to exit.");
// then...
break;
// ...or, if using Task:
// token.ThrowIfCancellationRequested();
}
}
}
Tenga en cuenta que debe llamar a Dispose en el origen de token vinculado cuando haya
terminado con él.
Cooperación entre código de biblioteca y código de usuario
El marco de cancelación unificada permite que el código de biblioteca cancele código de
usuario y que el código de usuario cancele código de biblioteca de manera cooperativa. Una
buena cooperación depende de que cada lado siga estas instrucciones:
mismo modelo es aplicable a las operaciones asincrónicas creadas directamente por el tipo
System.Threading.ThreadPool o System.Threading.Thread.
Ejemplo
El sondeo necesita algún tipo de bucle o código recursivo que pueda leer periódicamente el
valor de la propiedad booleana IsCancellationRequested. Si está usando el tipo
System.Threading.Tasks.Task y espera que la tarea se complete en el subproceso que realiza la
llamada, puede emplear el método ThrowIfCancellationRequested para comprobar la propiedad
y producir la excepción. Mediante este método, se asegura de que se produce la excepción
correcta como respuesta a una solicitud. Si está usando Task, es mejor llamar a este método que
producir manualmente una excepción OperationCanceledException. Si no tiene que producir la
excepción, simplemente puede comprobar la propiedad y volver del método si la propiedad es
true.
class CancelByPolling
{
static void Main()
{
var tokenSource = new CancellationTokenSource();
// Toy object for demo purposes
Rectangle rect = new Rectangle() { columns = 1000, rows = 500 };
// Simple cancellation scenario #1. Calling thread does not wait
// on the task to complete, and the user delegate simply returns
// on cancellation request without throwing.
Task.Run(() => NestedLoops(rect, tokenSource.Token),
tokenSource.Token);
// Simple cancellation scenario #2. Calling thread does not wait
// on the task to complete, and the user delegate throws
// OperationCanceledException to shut down task and transition its
state.
// Task.Run(() => PollByTimeSpan(tokenSource.Token),
tokenSource.Token);
Console.WriteLine("Press 'c' to cancel");
if (Console.ReadKey().KeyChar == 'c')
{
tokenSource.Cancel();
Console.WriteLine("Press any key to exit.");
}
Console.ReadKey();
}
class CancelWithCallback
{
static void Main(string[] args)
{
var cts = new CancellationTokenSource();
// Start cancelable task.
Task t = Task.Run(() =>
{
DoWork(cts.Token);
});
Console.WriteLine("Press 'c' to cancel.");
char ch = Console.ReadKey().KeyChar;
if (ch == 'c')
{
cts.Cancel();
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
class CancelOldStyleEvents
{
// Old-style MRE that doesn't support unified cancellation.
static ManualResetEvent mre = new ManualResetEvent(false);
Thread.SpinWait(5000000);
}
}
}
}
{
// Throw immediately to be responsive. The alternative is to
// do one more item of work, and throw on next iteration,
because
// IsCancellationRequested will be true.
Console.WriteLine("The wait operation was canceled.");
throw;
}
Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
}
}
}
class LinkedTokenSourceDemo
{
static void Main()
{
WorkerWithTimer worker = new WorkerWithTimer();
CancellationTokenSource cts = new CancellationTokenSource();
catch (OperationCanceledException e)
{
if (e.CancellationToken == cts.Token)
Console.WriteLine("Canceled from UI thread throwing
OCE.");
}
catch (AggregateException ae)
{
Console.WriteLine("AggregateException caught: " +
ae.InnerException);
foreach (var inner in ae.InnerExceptions)
{
Console.WriteLine(inner.Message + inner.Source);
}
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
class WorkerWithTimer
{
CancellationTokenSource internalTokenSource = new
CancellationTokenSource();
CancellationToken internalToken;
CancellationToken externalToken;
Timer timer;
public WorkerWithTimer()
{
internalTokenSource = new CancellationTokenSource();
internalToken = internalTokenSource.Token;
// A toy cancellation trigger that times out after 3 seconds
// if the user does not press 'c'.
timer = new Timer(new TimerCallback(CancelAfterTimeout), null,
3000, 3000);
}
{
for (int i = 0; i < 1000; i++)
{
if (token.IsCancellationRequested)
{
// We need to dispose the timer if cancellation
// was requested by the external token.
timer.Dispose();
// Throw the exception.
token.ThrowIfCancellationRequested();
}
// Simulating work.
Thread.SpinWait(7500000);
Console.Write("working... ");
}
}
Nota
Una vez iniciado un subproceso, no es necesario conservar una referencia al objeto Thread. El
subproceso se continúa ejecutando hasta que el procedimiento del mismo finaliza.
En el ejemplo de código siguiente se crean dos subprocesos nuevos para llamar a métodos
estáticos y una instancia de otro objeto.
using System;
using System.Threading;
// Delegate used to execute the callback method when the task is complete.
private ExampleCallback callback;
// The callback method must match the signature of the callback delegate.
public static void ResultCallback(int lineCount)
{
Console.WriteLine("Independent task printed {0} lines.", lineCount);
}
}
El método Thread.Sleep
Si se llama al método Thread.Sleep, el subproceso actual se bloquea inmediatamente durante el
número de milisegundos que se pasen a Thread.Sleep y el resto de su espacio de tiempo se
asigna a otro subproceso. Un subproceso no puede llamar a Thread.Sleep en otro subproceso.
Cuando se llama a Thread.Sleep con Timeout.Infinite, un subproceso pasa a un estado de
suspensión hasta que lo interrumpe otro subproceso que llama a Thread.Interrupt o hasta que
Thread.Abort finaliza dicho subproceso.
Interrumpir subprocesos
Puede interrumpir un subproceso en espera llamando a Thread.Interrupt en el subproceso
bloqueado para producir una excepción ThreadInterruptedException, que saca al subproceso de
la llamada que lo bloquea. El subproceso debe detectar la excepción
ThreadInterruptedException y hacer lo que sea necesario para seguir funcionando. Si el
subproceso pasa por alto la excepción, el motor en tiempo de ejecución detecta la excepción y
detiene el subproceso.
Nota
Si el subproceso de destino no está bloqueado cuando se llama a Thread.Interrupt, el subproceso
no se interrumpe hasta que se bloquea. Si el subproceso no se bloquea nunca, puede finalizar sin
ser interrumpido.
Si una espera es de tipo administrado, Thread.Interrupt y Thread.Abort activan el subproceso
inmediatamente. Si una espera es de tipo no administrado (por ejemplo, una llamada de
invocación de plataforma a la función WaitForSingleObject de Win32), ni Thread.Interrupt ni
Thread.Abort pueden tomar el control del subproceso hasta que éste vuelva o llame al código
administrado. En código administrado, el comportamiento es el siguiente:
Thread.Interrupt activa un subproceso de cualquier tipo de espera y hace que se produzca
una excepción ThreadInterruptedException en el subproceso de destino.
Thread.Abort es similar a Thread.Interrupt, con la diferencia de que hace que se produzca
una excepción ThreadAbortException en el subproceso.
Suspender y reanudar (obsoleto)
Importante
En la versión 2.0 de .NET Framework, los métodos Thread.Suspend y Thread.Resume están
marcados como obsoletos y, en futuras versiones, se quitarán estos métodos.
También puede pausar un subproceso si llama a Thread.Suspend. Cuando un subproceso llama a
Thread.Suspend en sí mismo, la llamada se bloquea hasta que el subproceso es reanudado por
otro subproceso. Cuando un subproceso llama a Thread.Suspend en otro subproceso, la llamada
no es de bloqueo y hace que el otro subproceso se pause. Al llamar a Thread.Resume, otro
subproceso sale del estado de suspensión y hace que el subproceso reanude la ejecución,
independientemente de cuántas veces se haya llamado a Thread.Suspend. Por ejemplo, si llama
a Thread.Suspend cinco veces consecutivas y, a continuación, llama a Thread.Resume, el
subproceso reanuda la ejecución inmediatamente después de llamar a Thread.Resume.
A diferencia de Thread.Sleep, Thread.Suspend no hace que un subproceso detenga
inmediatamente su ejecución. Common Language Runtime debe esperar hasta que el
subproceso alcance un punto de seguridad para poder suspender el subproceso. Un subproceso
no se puede suspender si no se ha iniciado o si se ha detenido.
Importante
Los métodos Thread.Suspend y Thread.Resume no suelen resultar útiles para las aplicaciones y
no deben confundirse con mecanismos de sincronización. Puesto que Thread.Suspend y
Thread.Resume no dependen de la cooperación del subproceso que se está controlando, son muy
intrusivos y pueden dar como resultado problemas graves de aplicación como interbloqueos (por
ejemplo, si se suspende un subproceso que contiene un recurso que necesitará otro subproceso).
Algunas aplicaciones necesitan controlar la prioridad de los subprocesos para obtener un mayor
rendimiento. Para ello, debe utilizar la propiedad Priority en lugar de Thread.Suspend.
1.2.3. Destruir subprocesos
El método Abort se utiliza para detener un subproceso administrado de forma permanente.
Cuando llama a Abort, Common Language Runtime inicia una ThreadAbortException en el
subproceso de destino, que puede detectar el subproceso de destino.
Nota
Si un subproceso está ejecutando código no administrado cuando se llama a su método Abort, el
motor en tiempo de ejecución lo marca como ThreadState.AbortRequested. Se inicia la
excepción cuando el subproceso vuelve al código administrado.
Una vez anulado un subproceso, no se puede reiniciar.
El método Abort no causa que el subproceso se anule inmediatamente, porque el subproceso de
destino puede detectar la ThreadAbortException y ejecutar cantidades arbitrarias de código en
un bloque finally. Puede llamar a Thread.Join si necesita esperar hasta que haya finalizado el
subproceso. Thread.Join es una llamada de bloqueo que no vuelve hasta que la ejecución del
subproceso se ha detenido realmente o ha transcurrido un tiempo de espera opcional. El
subproceso anulado podría llamar al método ResetAbort o realizar un procesamiento
independiente en un bloque finally, por lo que si no especifica un tiempo de espera, no existe
garantía de que finalice dicha espera.
Otros subprocesos que llaman a Thread.Interrupt pueden interrumpir subprocesos que están
esperando una llamada al método Thread.Join.
Controlar ThreadAbortException
Si espera que se vaya a anular su subproceso, bien como resultado de llamar a Abort desde su
propio código o como resultado de descargar un dominio de aplicación en el que se está
ejecutando el subproceso (AppDomain.Unload utiliza Thread.Abort para finalizar subprocesos),
su subproceso debe controlar la ThreadAbortException y realizar cualquier procesamiento final
en una cláusula finally, como se puede ver en el código siguiente.
try
{
// Code that is executing when the thread is aborted.
}
catch (ThreadAbortException ex)
{
// Clean-up code can go here.
// If there is no Finally clause, ThreadAbortException is
// re-thrown by the system at the end of the Catch clause.
}
// Do not put clean-up code here, because the exception
// is rethrown at the end of the Finally clause.
Su código de limpieza debe estar en la cláusula catch o en la cláusula finally, porque el sistema
vuelve a iniciar una ThreadAbortException al final de la cláusula finally, o al final de la cláusula
catch si no hay ninguna cláusula finally.
Puede evitar el sistema de vuelva a iniciar la excepción llamando al método Thread.ResetAbort.
Sin embargo, sólo debería hacerlo si la ThreadAbortException fue provocada por su propio
código.
1.2.4. Planear subprocesos
Cada subproceso tiene asignada una prioridad. Inicialmente, a los subprocesos creados en
Common Language Runtime se les asignan la prioridad ThreadPriority.Normal. Los
subprocesos creados fuera del motor en tiempo de ejecución mantienen la prioridad que tenían
antes de entrar en el entorno administrado. Con la propiedad Thread.Priority, puede obtener o
establecer la prioridad de cualquier subproceso.
La ejecución de los subprocesos se planea en función de su prioridad. Aunque los subprocesos
se ejecuten dentro del motor en tiempo de ejecución, el sistema operativo asigna espacios de
tiempo de procesador a todos los subprocesos. Los detalles del algoritmo utilizado para
determinar el orden en el que se ejecutan los subprocesos varían con cada sistema operativo. En
algunos sistemas operativos, el subproceso de mayor prioridad (o aquellos subprocesos que
pueden ejecutarse) se programa siempre para ejecutarse primero. Si hay disponibles varios
subprocesos con la misma prioridad, el programador recorre los subprocesos con dicha prioridad
y les concede a cada uno un espacio de tiempo fijo durante el que ejecutarse. Siempre que esté
disponible un subproceso de mayor prioridad para ejecutarse, no se ejecutan los subprocesos de
menor prioridad. Cuando no hay más subprocesos ejecutables con una prioridad dada, el
programador pasa a la siguiente prioridad y programa los subprocesos de dicha prioridad para
ejecutarse. Si un subproceso de prioridad superior pasa a poder ejecutarse, se adelanta al
subproceso de menor prioridad y se permite al subproceso de mayor prioridad volver a
ejecutarse una vez más. Sobre todo, el sistema operativo puede ajustar también prioridades de
subprocesos de forma dinámica cuando una interfaz de usuario de aplicación pasa a ejecutarse
de primer a segundo plano. Otros sistemas operativos podrían usar un algoritmo de
programación diferente.
1.2.5. Cancelar subprocesos de manera cooperativa
Con anterioridad a .NET Framework 4, .NET Framework no proporcionaba ningún medio
integrado para cancelar un subproceso de forma cooperativa una vez iniciado. Sin embargo, en
.NET Framework 4 se pueden utilizar tokens de cancelación para cancelar subprocesos; también
se pueden usar para cancelar objetos System.Threading.Tasks.Task o consultas PLINQ. Aunque
la clase System.Threading.Thread no proporciona compatibilidad integrada para tokens de
cancelación, se puede pasar un token a un procedimiento de subproceso utilizando el constructor
Thread que toma un delegado ParameterizedThreadStart. En el ejemplo siguiente se muestra
cómo hacerlo:
namespace CancelThreads
{
using System;
using System.Threading;
Condiciones de carrera
Una condición de carrera es un error que se produce cuando el resultado de un programa
depende del primero de dos o más subprocesos que consiga llegar hasta un bloque específico de
código. Ejecutar el programa muchas veces genera distintos resultados y no es posible predecir
el resultado de una ejecución específica.
momento difícil permitiendo, algunas veces, que otro subproceso llegue el primero al
bloque de código.
Miembros estáticos y constructores estáticos
No se inicializa una clase hasta que su constructor de clase (constructorstatic en C#, Shared Sub
New en Visual Basic) haya terminado de ejecutarse. Para evitar la ejecución de código en un
tipo no inicializado, Common Language Runtime bloquea todas las llamadas de otros
subprocesos a los miembros static de la clase (miembrosShared en Visual Basic) hasta que el
constructor de clase termina de ejecutarse.
Por ejemplo, si un constructor de clase inicia un nuevo subproceso, y el procedimiento del
subproceso llama a un miembro static de la clase, el nuevo subproceso se bloquea hasta que el
constructor de clase finalice.
Esto se aplica a cualquier tipo que pueda tener un constructor static.
Recomendaciones generales
Tenga en cuenta las siguientes instrucciones cuando utilice varios subprocesos:
No utilice Thread.Abort para finalizar otros subprocesos. Una llamada a Abort en otro
subproceso es similar a iniciar una excepción en ese subproceso, sin conocer qué punto
ha alcanzado en su procesamiento.
No utilice Thread.Suspend ni Thread.Resume para sincronizar las actividades de varios
subprocesos. Utilice Mutex, ManualResetEvent, AutoResetEvent y Monitor.
No controle la ejecución de subprocesos de trabajo desde el programa principal (con
eventos, por ejemplo). En su lugar, diseñe un programa de forma que los subprocesos de
trabajo sean los que tengan que esperar hasta que haya trabajo disponible, lo ejecuten y
notifiquen su finalización a otras partes del programa. Si los subprocesos de trabajo no
se bloquean, puede ser conveniente usar subprocesos del grupo de subprocesos.
Monitor.PulseAll resulta útil en aquellas situaciones en las que los subprocesos de
trabajo se bloquean.
No utilice los tipos como objetos de bloqueo. Es decir, evite código como
lock(typeof(X)) en C# o SyncLock(GetType(X)) en Visual Basic o el uso de
Monitor.Enter con objetos Type. Para un tipo determinado, hay sólo una instancia de
System.Type por el dominio de aplicación. Si el tipo que quiere bloquear es público,
codifique uno que su tipo no pueda bloquear, para evitar interbloqueos.
Tenga cuidado al efectuar bloqueos en instancias, por ejemplo lock(this) en C# o
SyncLock(Me) en Visual Basic. Si otra parte del código de la aplicación, ajeno al tipo,
bloquea el objeto, podrían producirse interbloqueos.
Asegúrese de que un subproceso que entra en un monitor siempre sale de ese monitor,
aun en el caso de que se produzca una excepción mientras el subproceso se encuentra en
el monitor. La instrucción lock de C# y la instrucción SyncLock de Visual Basic
ofrecen automáticamente este comportamiento mediante un bloque finally que garantiza
la llamada a Monitor.Exit. Si no está seguro de que se llamará a Exit, considere la
posibilidad de cambiar el diseño con el fin de utilizar Mutex. Una zona de exclusión
mutua se libera automáticamente cuando finaliza el subproceso al que pertenece.
Utilice varios subprocesos para tareas que requieren recursos diferentes, y evite asignar
varios subprocesos a un solo recurso. Por ejemplo, en tareas que impliquen beneficios
de E/S por tener un subproceso propio, ya que ese subproceso se bloquea durante las
operaciones de E/S y, de este modo, permite ejecutar otros subprocesos. Los datos
proporcionados por el usuario son otro recurso que se beneficia de la utilización de un
subproceso dedicado. En un equipo de un solo procesador, una tarea que implica un
cálculo intensivo coexiste con los datos proporcionados por el usuario y con tareas que
implican la E/S, pero varias tareas de cálculo intensivo compiten entre ellas.
Considere la posibilidad de utilizar métodos de la clase Interlocked para los cambios de
estado simples, en lugar de utilizar la instrucción lock (SyncLock en Visual Basic). La
instrucción lock es una buena herramienta de uso general, pero la clase Interlocked
genera mejor rendimiento para las actualizaciones que deben ser atómicas.
Internamente, ejecuta un solo prefijo de bloqueo si no hay contención. En las revisiones
de código, inspeccione código similar al que se muestra en los ejemplos siguientes. En
el primer ejemplo, se incrementa una variable de estado:
lock(lockObject)
{
myField++;
}
hay muy pocos subprocesos, no se haga un uso óptimo de los recursos disponibles, mientras que
demasiados subprocesos podrían aumentar la contención de recursos.
Precaución
Puede usar el método SetMinThreads para aumentar el número mínimo de subprocesos
inactivos. Sin embargo, si estos valores se incrementan innecesariamente, pueden producirse
problemas de rendimiento. Si se inician demasiadas tareas al mismo tiempo, puede parecer que
todas se ejecutan con lentitud. En la mayoría de los casos, el grupo de subprocesos funcionará
mejor con su propio algoritmo de asignación de subprocesos.
Omitir las comprobaciones de seguridad
El grupo de subprocesos también proporciona los métodos
ThreadPool.UnsafeQueueUserWorkItem y ThreadPool.UnsafeRegisterWaitForSingleObject.
Utilice estos métodos únicamente si está seguro de que pila del llamador no es pertinente para
las comprobaciones de seguridad realizadas durante la ejecución de la tarea en cola.
QueueUserWorkItem y RegisterWaitForSingleObject capturan la pila del llamador, que se
combina en la pila del subproceso del grupo de subprocesos cuando el subproceso empieza a
ejecutar una tarea. Si es necesario realizar una comprobación de seguridad, debe comprobarse
toda la pila. Aunque la comprobación proporciona seguridad, es a costa del rendimiento.
Utilizar el grupo de subprocesos
A partir de .NET Framework 4, el mecanismo más sencillo para usar el grupo de subprocesos
consiste en usar Biblioteca de procesamiento paralelo basado en tareas (TPL). De forma
predeterminada, los tipos bibliotecas paralelas, como Task y Task<TResult>, usan los
subprocesos del grupo de subprocesos para ejecutar tareas. También puede usar el grupo de
subprocesos llamando a ThreadPool. QueueUserWorkItem desde el código administrado (o a
CorQueueUserWorkItem desde el código no administrado) y pasando un delegado
WaitCallback que representa el método que realiza la tarea. Otra forma de usar el grupo de
subprocesos consiste en poner en cola los elementos de trabajo que están relacionados con una
operación de espera usando el método ThreadPool.RegisterWait ForSingleObject y pasando
WaitHandle que, cuando se señala o agota el tiempo de espera, llama al método representado
por el delegado WaitOrTimerCallback. Los subprocesos del grupo de subprocesos se usan para
invocar métodos de devolución de llamada.
Ejemplos de ThreadPool
En los ejemplos de código de esta sección se muestra el grupo de subprocesos usando la clase
Task, el método ThreadPool.QueueUserWorkItem y el método
ThreadPool.RegisterWaitForSingleObject.
Ejecutar tareas asincrónicas con la biblioteca TPL
Ejecutar código asincrónicamente con QueueUserWorkItem
Proporcionar datos de tareas para QueueUserWorkItem
Usar RegisterWaitForSingleObject
Ejecutar tareas asincrónicas con la biblioteca TPL
En el ejemplo siguiente se muestra cómo se crea y se usa un objeto Task llamando al método
TaskFactory.StartNew.
using System;
using System.Threading;
using System.Threading.Tasks;
class StartNewDemo
{
// Demonstrated features:
// Task ctor()
// Task.Factory
// Task.Wait()
// Task.RunSynchronously()
// Expected results:
// Task t1 (alpha) is created unstarted.
// Task t2 (beta) is created started.
// Task t1's (alpha) start is held until after t2 (beta) is
started.
// Both tasks t1 (alpha) and t2 (beta) are potentially executed
on
// threads other than the main thread on multi-core machines.
// Task t3 (gamma) is executed synchronously on the main thread.
// Documentation:
// http://msdn.microsoft.com/en-
us/library/system.threading.tasks.task_members(VS.100).aspx
static void Main()
{
Action<object> action = (object obj) =>
{
Console.WriteLine("Task={0}, obj={1}, Thread={2}", Task.CurrentId,
obj.ToString(), Thread.CurrentThread.ManagedThreadId);
};
// Construct an unstarted task
Task t1 = new Task(action, "alpha");
// Cosntruct a started task
Task t2 = Task.Factory.StartNew(action, "beta");
// Block the main thread to demonstate that t2 is executing
t2.Wait();
// Launch t1
t1.Start();
Console.WriteLine("t1 has been launched. (Main Thread={0})",
Thread.CurrentThread.ManagedThreadId);
// Wait for the task to finish.
// You may optionally provide a timeout interval or a cancellation
token
// to mitigate situations when the task takes too long to finish.
t1.Wait();
// Construct an unstarted task
Task t3 = new Task(action, "gamma");
// Run it synchronously
t3.RunSynchronously();
// Although the task was run synchrounously, it is a good practice to
wait
// for it which observes for exceptions potentially thrown by that
task.
t3.Wait();
}
}
}
// This thread procedure performs the task.
static void ThreadProc(Object stateInfo)
{
// No state object was passed to QueueUserWorkItem, so
// stateInfo is null.
Console.WriteLine("Hello from the thread pool.");
}
}
Usar RegisterWaitForSingleObject
En el ejemplo siguiente se muestran diversas características del subprocesamiento.
Poner en cola una tarea para su ejecución por medio de subprocesos ThreadPool, con el
método RegisterWaitForSingleObject.
Señalar una tarea para su ejecución, con AutoResetEvent.
Controlar los tiempos de espera y las señales con un delegado WaitOrTimerCallback.
Cancelar una tarea de la cola con RegisteredWaitHandle.
using System;
using System.Threading;
// The callback method executes when the registered wait times out,
// or when the WaitHandle (in this case AutoResetEvent) is signaled.
// WaitProc unregisters the WaitHandle the first time the event is
signaled.
public static void WaitProc(object state, bool timedOut)
{
// The state object must be cast to the correct type, because the
// signature of the WaitOrTimerCallback delegate specifies type
Object.
TaskInfo ti = (TaskInfo) state;
string cause = "TIMED OUT";
if (!timedOut)
{
cause = "SIGNALED";
// If the callback method executes because the WaitHandle is
// signaled, stop future execution of the callback method
// by unregistering the WaitHandle.
if (ti.Handle != null) ti.Handle.Unregister(null);
}
Console.WriteLine("WaitProc( {0} ) executes on thread {1}; cause =
{2}.",
ti.OtherInfo,Thread.CurrentThread.GetHashCode().ToString(),cause);
}
}
1.4.2. Temporizadores
Los temporizadores son objetos pequeños que permiten especificar un delegado para llamarlo en
un momento específico. Un subproceso del grupo realiza la operación de espera.
El uso de la clase Timer es sencillo. Para crear un Timer, debe pasar un delegado
TimerCallback al método de devolución de llamada, un objeto que representa el estado que se
pasará a la devolución de llamada, la hora de compilación inicial y una cifra que representa el
período entre invocaciones de devolución de llamada. Para cancelar un temporizador pendiente,
se debe llamar a la función Timer.Dispose.
Nota
Hay otras dos clases de temporizador. La clase System.Windows.Forms.Timer es un control que
funciona con diseñadores visuales y está pensado para su uso en contextos de interfaz de
usuario; provoca eventos en el subproceso de interfaz de usuario. La clase System.Timers.Timer
deriva de Component, por lo que puede usarse con diseñadores visuales; también provoca
eventos, pero lo hace en un subproceso ThreadPool. La clase System.Threading.Timer realiza
las devoluciones de llamada en un subproceso ThreadPool y no utiliza el modelo de evento para
nada. También proporciona un objeto de estado al método de devolución de llamada, lo que no
hacen otros temporizadores. Es sumamente ligero.
El ejemplo de código siguiente inicia un temporizador que se inicia después de un segundo
(1000 milisegundos) y emite un chasquido por segundo hasta que presione la tecla Entrar. La
variable que contiene la referencia al temporizador es un campo de nivel de clase para garantizar
que el temporizador no está sujeto a la recolección de elementos no utilizados mientras que
todavía está en ejecución.
using System;
using System.Threading;
1.4.3. Monitores
Los objetos Monitor exponen la capacidad de sincronizar el acceso a una región de código
tomando y liberando un bloqueo de un objeto concreto mediante los métodos Monitor.Enter,
Monitor.TryEnter y Monitor.Exit. Una vez que se tenga un bloqueo en una región del código, se
podrán usar los métodos Monitor.Wait, Monitor.Pulse y Monitor.PulseAll. Wait libera el
bloqueo si este se mantiene y espera recibir una notificación. Cuando el método Wait recibe la
notificación, devuelve y obtiene de nuevo el bloqueo. Ambos métodos, Pulse y PulseAll avisan
al siguiente subproceso de la cola de espera de que puede continuar.
Las instrucciones SyncLock de Visual Basic y lock de C# utilizan Monitor.Enter para realizar el
bloqueo y Monitor.Exit para liberarlo. La ventaja de utilizar las instrucciones de un lenguaje es
que todo lo que esté incluido en el bloqueo lock o SyncLock está incluido en una instrucción
Try. La instrucción Try tiene un bloque Finally para garantizar que se libera el bloqueo.
Monitor bloquea objetos (es decir, tipos de referencia), no tipos de valor. Aunque puede pasar
un tipo de valor a Enter y Exit, la conversión boxing se aplica por separado en cada llamada.
Como cada llamada crea un objeto independiente, Enter nunca se bloquea, y el código que
supuestamente está protegiendo no está en realidad sincronizado. Además, el objeto que se pasa
a Exit es distinto del objeto que se pasa a Enter, por tanto, Monitor inicia una excepción
SynchronizationLockException con el mensaje "El método de sincronización del objeto se ha
llamado desde un bloque de códigos sin sincronizar". En el siguiente ejemplo se ilustran estos
problemas.
try
{
int x = 1;
// The call to Enter() creates a generic synchronizing object for the
value
// of x each time the code is executed, so that Enter never blocks.
Monitor.Enter(x);
try
{
// Code that needs to be protected by the monitor.
}
finally
{
// Always use Finally to ensure that you exit the Monitor.
// The call to Exit() will FAIL!!!
// The synchronizing object created for x in Exit() will be different
// than the object used in Enter(). SynchronizationLockException
// will be thrown.
Monitor.Exit(x);
}
}
catch (SynchronizationLockException SyncEx)
{
Console.WriteLine("A SynchronizationLockException occurred. Message:");
Console.WriteLine(SyncEx.Message);
}
Aunque se puede aplicar una conversión boxing a una variable de tipo de valor antes de llamar a
Enter y a Exit, tal y como se muestra en el ejemplo siguiente, y pasar el mismo objeto al que se
le ha aplicado la conversión boxing a los dos métodos, hacerlo no reporta ningún beneficio. Los
cambios en la variable no se reflejan en la copia a la que se aplica la conversión boxing y no hay
ninguna forma de cambiar el valor de esta copia.
int x = 1;
object o = x;
Monitor.Enter(o);
try
{
// Code that needs to be protected by the monitor.
}
finally
{
// Always use Finally to ensure that you exit the Monitor.
Monitor.Exit(o);
}
Es importante tener en cuenta la distinción entre el uso de objetos Monitor y WaitHandle. Los
objetos Monitor están estrictamente administrados, son totalmente portátiles y podrían ser más
eficaces en lo que respecta a los requisitos de recursos del sistema operativo. Los objetos
WaitHandle representan objetos de espera del sistema operativo, son útiles para la
// Without the lock, the method is called in the order in which threads reach
it.
class UnSyncResource
{
public void Access(Int32 threadNum)
{
// Does not use Monitor class to enforce synchronization.
// The next call throws the thread order.
if (threadNum % 2 == 0)
{
Thread.Sleep(2000);
}
Console.WriteLine("Start UnSynched Resource access (Thread={0})",
threadNum);
Thread.Sleep(200);
Console.WriteLine("Stop UnSynched Resource access (Thread={0})",
threadNum);
}
}
ThreadPool.QueueUserWorkItem(new WaitCallback(SyncUpdateResource),
threadNum);
}
// Wait until this WaitHandle is signaled.
asyncOpsAreDone.WaitOne();
Console.WriteLine("\t\nAll synchronized operations have
completed.\t\n");
// Reset the thread count for unsynchronized calls.
numAsyncOps = 5;
for (Int32 threadNum = 0; threadNum < 5; threadNum++)
{
ThreadPool.QueueUserWorkItem(new
WaitCallback(UnSyncUpdateResource),
threadNum);
}
// Wait until this WaitHandle is signaled.
asyncOpsAreDone.WaitOne();
Console.WriteLine("\t\nAll unsynchronized thread operations have
completed.");
}
Los controladores de espera de eventos no son eventos en el sentido en que se utiliza este
término en .NET Framework; no hay delegados ni controladores de eventos implicados. Se
utiliza el término "evento" para describirlos porque tradicionalmente se les conocía como
eventos del sistema operativo y porque el acto de señalizar el controlador de espera indica a los
subprocesos en espera que se ha producido un evento.
Tanto los controladores de espera de eventos locales como los de eventos con nombre utilizan
objetos de sincronización del sistema, que están protegidos mediante contenedores
SafeWaitHandle para garantizar que se liberen los recursos. Puede utilizar el método
IDisposable.Dispose para liberar inmediatamente los recursos cuando haya terminado de utilizar
el objeto.
Controladores de espera de eventos de restablecimiento automático
Puede crear un evento de restablecimiento automático especificando
EventResetMode.AutoReset al crear el objeto EventWaitHandle. Como su propio nombre
indica, este evento de sincronización se restablece automáticamente cuando se señaliza, después
de liberar un único subproceso en espera. Señalice el evento llamando a su método Set.
Los eventos de restablecimiento automático se utilizan normalmente para proporcionar acceso
exclusivo a un recurso a un único subproceso cada vez. Un subproceso solicita el recurso
llamando al método WaitOne. Si no hay ningún otro subproceso que contenga el controlador de
espera, el método devolverá true y el subproceso de llamada tomará el control del recurso.
Importante
Como ocurre con todos los mecanismos de sincronización, deberá asegurarse de que todas las
rutas de acceso al código se mantengan a la espera en el controlador de espera adecuado antes
de obtener acceso a un recurso protegido. La sincronización de subprocesos es operación
cooperativa.
Si un evento de restablecimiento automático se señaliza cuando no hay ningún subproceso en
espera, permanecerá señalizado hasta que algún subproceso intente esperar en él. El evento
liberará el subproceso e, inmediatamente después, se restablecerá, de modo que bloqueará los
subprocesos posteriores.
Controladores de espera de eventos de restablecimiento manual
Puede crear un evento de restablecimiento manual especificando EventResetMode.ManualReset
al crear el objeto EventWaitHandle. Cuando su propio nombre indica, este evento de
sincronización debe restablecerse manualmente tras su señalización. Hasta que no se restablezca
mediante una llamada al método Reset, los subprocesos que se mantengan a la espera en el
controlador de eventos se ejecutarán inmediatamente, sin bloquearse.
El funcionamiento de un evento de restablecimiento manual se asemeja al de la puerta de un
establo. Si el evento no está señalizado, los subprocesos en espera se bloquean mutuamente,
como los caballos de un establo. Cuando el evento se señaliza mediante una llamada al método
Set, todos los subprocesos en espera quedan libres para su ejecución. El evento permanece
señalizado hasta que se llama al método Reset. Esto hace del evento de restablecimiento manual
una forma ideal de retener subprocesos que deben esperar a que otro subproceso finalice una
tarea.
Como en el ejemplo de los caballos que salen del establo, el sistema operativo tarda cierto
tiempo en programar los subprocesos liberados y reanudar su ejecución. Si se llama al método
Reset antes de que se haya reanudado la ejecución de todos los subprocesos, los subprocesos
restantes volverán a bloquearse. No se puede determinar qué procesos se reanudarán y cuáles se
bloquearán ya que depende de una serie de factores aleatorios, como la carga del sistema, el
número de subprocesos en espera de programación, etc. Esto no supone ningún problema si el
subproceso que señaliza el evento finaliza tras la señalización, que es el patrón de uso más
común. Si desea que el subproceso que señalizaba el evento inicie una nueva tarea una vez
reanudados todos los subprocesos en espera, deberá bloquearlo hasta que se hayan reanudado
todos los subprocesos en espera. De lo contrario, se producirá una condición de carrera y el
comportamiento del código será imprevisible.
Características comunes de los eventos automáticos y manuales
Normalmente, uno o varios subprocesos se bloquean en EventWaitHandle hasta que un
subproceso desbloqueado llama al método Set, que libera uno de los subprocesos en espera (en
el caso de los eventos de restablecimiento automático) o todos los subprocesos (en el caso de los
eventos de restablecimiento manual). Un subproceso puede señalizar un controlador
EventWaitHandle y, a continuación, bloquearse en el mismo, como una operación atómica,
llamando al método WaitHandle.SignalAndWait estático.
Los objetos EventWaitHandle pueden utilizarse con los métodos WaitHandle.WaitAll y
WaitHandle.WaitAny estáticos. Como las clases EventWaitHandle y Mutex se derivan de
WaitHandle, se pueden utilizar ambas clases con estos métodos.
Eventos con nombre
El sistema operativo Windows permite que los controladores de espera de eventos tengan
nombres. Un evento con nombre afecta a todo el sistema, es decir, una vez que se crea, es
visible para todos los subprocesos en todos los procesos. Por tanto, los eventos con nombre
pueden utilizarse para sincronizar las actividades de los procesos y de los subprocesos.
Se puede crear un objeto EventWaitHandle que represente un evento con nombre del sistema
utilizando uno de los constructores que especifica un nombre de evento.
Nota
Como los eventos con nombre afectan a todo el sistema, es posible disponer de varios objetos
EventWaitHandle que representen al mismo evento con nombre. Cada vez que se llame a un
constructor, o al método OpenExisting, se creará un nuevo objeto EventWaitHandle. Si se
especifica repetidamente el mismo nombre, se crean varios objetos que representan el mismo
evento con nombre.
Se recomienda utilizar con precaución los eventos con nombre. Como son válidos para todo el
sistema, si otro proceso utiliza el mismo nombre podrían bloquearse inesperadamente los
subprocesos. Cualquier código malintencionado ejecutado en el mismo equipo podría utilizar
esto como base de un ataque por denegación de servicio.
Utilice la seguridad de control de acceso para proteger un objeto EventWaitHandle que
represente un evento con nombre, preferiblemente utilizando un constructor que especifique un
objeto EventWaitHandleSecurity. También puede aplicar la seguridad de control de acceso
mediante el método SetAccessControl, pero daría cabida a un margen de vulnerabilidad entre el
momento de creación del controlador de espera de eventos y el momento de su protección. La
protección de eventos por medio de la seguridad de control de acceso contribuye a evitar
ataques malintencionados, pero no resuelve el problema de conflictos de nombres involuntarios.
Nota
A diferencia de la clase EventWaitHandle, las clases derivadas AutoResetEvent y ManualReset
Event sólo pueden representar controladores de espera locales. No pueden representar eventos
con nombre del sistema.
1.4.5.2. AutoResetEvent
La clase AutoResetEvent representa un evento de identificador de espera local que se restablece
automáticamente cuando se le señala, después de liberar un subproceso en espera único. Esta
clase es un caso especial de su clase base, EventWaitHandle. Consulte la documentación
conceptual EventWaitHandle para obtener información sobre el uso y las características de los
eventos de restablecimiento automático.
class DataWithToken
{
public CancellationToken Token { get; set; }
public Data Data { get; private set; }
public DataWithToken(Data data, CancellationToken ct)
{
this.Data = data;
this.Token = ct;
}
}
Observe que la operación de espera no cancela los subprocesos que la señalan. Normalmente, la
cancelación se aplica a una operación lógica, lo que puede incluir esperar el evento y todos los
elementos de trabajo que la espera esté sincronizando. En este ejemplo, a cada elemento de
trabajo se pasa una copia del mismo token de cancelación para que pueda responder a la
solicitud de cancelación.
1.4.6. Exclusiones mutuas (mutex)
Puede utilizar un objeto Mutex para proporcionar acceso exclusivo a un recurso. La clase Mutex
utiliza más recursos del sistema que la clase Monitor, pero pueden calcularse las referencias de
la misma a través de los límites del dominio de aplicación, puede utilizarse con varias esperas y
puede utilizarse para sincronizar subprocesos de distintos procesos. Encontrará una comparación
de los mecanismos de sincronización administrados en Información general sobre los primitivos
de sincronización.
Usar exclusiones mutuas
Un subproceso llama al método WaitOne de una exclusión mutua para solicitar su propiedad. La
llamada se bloquea hasta que la exclusión mutua queda disponible o hasta que transcurre el
intervalo de tiempo de espera opcional. Si una exclusión mutua no pertenece a ningún
subproceso, el estado de la misma se señaliza.
Un subproceso libera una exclusión mutua llamando a su método ReleaseMutex. Las
exclusiones mutuas cuentan con afinidad de subproceso; es decir, el subproceso que posee la
exclusión mutua es el único que puede liberarla. Si un subproceso libera una exclusión mutua
que no posee, se produce una excepción ApplicationException en el subproceso.
Como la clase Mutex se deriva de WaitHandle, también puede llamar al método estático
WaitAll o WaitAny de WaitHandle para solicitar la propiedad de Mutex en combinación con
otros controladores de espera.
Si un subproceso posee un objeto Mutex, dicho subproceso puede especificar el mismo Mutex
en llamadas repetidas de solicitud de espera sin bloquear su ejecución; no obstante, deberá
liberar Mutex las veces que sean necesarias para liberar su propiedad.
Exclusiones mutuas abandonadas
Si un subproceso finaliza sin liberar Mutex, la exclusión mutua se considera abandonada. Esto
normalmente pone de manifiesto un error de programación grave porque el recurso al que
protege la exclusión mutua podría quedarse en un estado incoherente. En la versión 2.0 de .NET
Framework, se produce una excepción AbandonedMutexException en el siguiente subproceso
que adquiere la exclusión mutua.
Nota
En las versiones 1.0 y 1.1 de .NET Framework, un objeto Mutex abandonado se establece en
estado señalado y el siguiente subproceso en espera obtiene su propiedad. Si no hay ningún
subproceso en espera, Mutex permanece en estado señalado. No se produce ninguna excepción.
En el caso de una exclusión mutua en todo el sistema, una exclusión mutua abandonada podría
indicar que una aplicación ha finalizado de forma abrupta (por ejemplo, mediante el
Administrador de tareas de Windows).
Exclusiones mutuas locales y del sistema
Las exclusiones mutuas son de dos tipos: exclusiones mutuas locales y exclusiones mutuas del
sistema con nombre. Si crea un objeto Mutex mediante el uso de un constructor que acepta un
nombre, quedará asociado a un objeto del sistema operativo con ese nombre. Las exclusiones
mutuas del sistema con nombre son visibles en todo el sistema operativo y pueden utilizarse
para sincronizar las actividades de los procesos. Puede crear varios objetos Mutex que
representen la misma exclusión mutua del sistema con nombre y puede utilizar el método
OpenExisting para abrir una exclusión mutua del sistema con nombre existente.
Una exclusión mutua local sólo existe dentro de su proceso. Puede utilizarla cualquier
subproceso del proceso que tenga una referencia al objeto Mutex local. Cada objeto Mutex es
una exclusión mutua local independiente.
Seguridad de control de acceso para exclusiones mutuas del sistema
La versión 2.0 de .NET Framework proporciona la posibilidad de consultar y establecer
seguridad de control de acceso de Windows para los objetos del sistema con nombre. Es
recomendable proteger las exclusiones mutuas del sistema desde el momento de su creación
porque los objetos del sistema son globales y, por lo tanto, un código distinto del suyo propio es
capaz de bloquearlas.
Para obtener información sobre la seguridad de control de acceso para las exclusiones mutuas,
vea las clases MutexSecurity y MutexAccessRule, la enumeración MutexRights, los métodos
GetAccessControl, SetAccessControl y OpenExisting de la clase Mutex y el constructor
Mutex(Boolean, String, Boolean, MutexSecurity).
1.4.7. Operaciones de bloqueo
La clase Interlocked proporciona métodos que sincronizan el acceso a una variable compartida
por varios subprocesos. Los subprocesos de diferentes procesos pueden utilizar este mecanismo
si la variable está en la memoria compartida. Las operaciones de bloqueo son atómicas, es decir,
el conjunto de la operación constituye una unidad que ninguna otra operación de bloqueo puede
interrumpir en la misma variable. Esto último tiene importancia en sistemas operativos con
multithreading preferente, donde se puede suspender un subproceso después de cargar un valor
desde una dirección de memoria, pero antes de tener oportunidad de alterarlo y almacenarlo.
La clase Interlocked proporciona las siguientes operaciones:
En la versión 2.0 de .NET Framework, el método Add agrega un valor entero a una
variable y devuelve el nuevo valor de la variable.
En la versión 2.0 de .NET Framework, el método Read lee un valor entero de 64 bits
como una operación atómica. Esto resulta útil en sistemas operativos de 32 bits, donde
leer un entero de 64 bits no constituye habitualmente una operación atómica.
Los métodos Increment y Decrement incrementan o decrementan una variable y
devuelven el valor resultante.
El método Exchange realiza un intercambio atómico del valor en la variable
especificada, devolviendo dicho valor y reemplazándolo por un valor nuevo. En la
versión 2.0 de .NET Framework, puede utilizarse una sobrecarga genérica de este
método para realizar este intercambio en una variable con cualquier tipo de referencia.
El método CompareExchange también intercambia dos valores, pero en función del
resultado de una comparación. En la versión 2.0 de .NET Framework, puede utilizarse
una sobrecarga genérica de este método para realizar este intercambio en una variable
con cualquier tipo de referencia.
En los procesadores modernos, los métodos de la clase Interlocked a menudo pueden
implementarse mediante una única instrucción. Así, proporcionan una sincronización con un
rendimiento muy elevado y pueden utilizarse para compilar mecanismos de sincronización de
nivel superior, como bloqueos circulares.
Ejemplo de CompareExchange
El método CompareExchange se puede utilizar para proteger cálculos más complicados que un
simple incremento y decremento. En el siguiente ejemplo se muestra un método seguro para
subprocesos que se agrega a un total actualizado almacenado como un número de punto flotante.
(En el caso de los números enteros, el método Add constituye una solución más sencilla.) Para
obtener ejemplos de código completos, vea las sobrecargas de CompareExchange que toma
Nota
.NET Framework dispone de dos bloqueos de lector y escritor, ReaderWriterLockSlim y
ReaderWriterLock. ReaderWriterLockSlim se recomienda para todos los trabajos de desarrollo
nuevos. ReaderWriterLockSlim es similar a ReaderWriterLock, pero tiene reglas simplificadas
para la recursividad y para actualizar y degradar el estado del bloqueo. ReaderWriterLockSlim
evita muchos casos de interbloqueo potencial. Además, el rendimiento de
ReaderWriterLockSlim es significativamente mejor que ReaderWriterLock.
Puede crear un objeto Semaphore que represente un semáforo de sistema con nombre utilizando
uno de los constructores que especifica un nombre.
Nota
Dado que los semáforos con nombre son semáforos para todo el sistema, es posible tener varios
objetos Semaphore que representen el mismo semáforo con nombre. Cada vez que se llama a un
constructor o al método Semaphore.OpenExisting, se crea un nuevo objeto Semaphore. Si se
especifica el mismo nombre repetidas veces, se crean varios objetos que representan el mismo
semáforo con nombre.
Tenga el cuidado al utilizar los semáforos con nombre. Dado que son semáforos para todo el
sistema, puede ocurrir que otro proceso que utilice el mismo nombre entre inesperadamente en
el semáforo. Cualquier código malintencionado ejecutado en el mismo equipo podría utilizar
esto como base de un ataque por denegación de servicio.
Utilice la seguridad de control de acceso para proteger un objeto Semaphore que represente un
semáforo con nombre, preferentemente utilizando un constructor que especifique un objeto
System.Security.AccessControl.SemaphoreSecurity. También puede aplicar la seguridad de
control de acceso mediante el método Semaphore.SetAccessControl, aunque este sistema dejará
un espacio de vulnerabilidad entre el momento en que se crea el semáforo y el momento en que
se protege. Proteger los semáforos con seguridad de control de acceso ayuda a evitar ataques
malintencionados, pero no resuelve el problema de los conflictos de nombres no intencionados.
1.4.10. Información general sobre los primitivos de sincronización
.NET Framework proporciona un intervalo de primitivos de sincronización para controlar las
interacciones de subprocesos y evitar las condiciones de carrera. Éstos se pueden dividir
básicamente en tres categorías: operaciones de bloqueo, señalización e interbloqueo.
La definición de estas categorías no es clara ni nítida: algunos mecanismos de sincronización
tienen características de varias categorías; los eventos que liberan un único subproceso a la vez
actúan funcionalmente como bloqueos; la liberación de cualquier bloqueo se puede considerar
como una señal y las operaciones de interbloqueo se pueden usar para construir bloqueos. Sin
embargo, las categorías siguen siendo útiles.
Es importante recordar que la sincronización de subprocesos es cooperativa. Incluso si un
subproceso omite un mecanismo de sincronización y tiene acceso directamente al recurso
protegido, ese mecanismo de la sincronización no puede ser eficaz.
Bloqueo
Los bloqueos proporcionan el control de un recurso a un subproceso cada vez, o a un número
especificado de subprocesos. Un subproceso que solicita un bloqueo exclusivo cuando el
bloqueo está en uso queda bloqueado hasta que el bloqueo está disponible.
Bloqueos exclusivos
La forma más sencilla de bloqueo es la instrucción lock de C# (SyncLock en Visual Basic), que
controla el acceso a un bloque de código. Este tipo de bloque frecuentemente se suele
denominar sección crítica. La instrucción lock se implementa utilizando los métodos Enter y
Exit de la clase Monitor, y utiliza try…catch…finally para garantizar que se libera el bloqueo.
En general, utilizar la instrucción lock para proteger pequeños bloques de código, sin abarcar
nunca más de un único método, es la mejor manera de usar la clase Monitor. Aunque eficaz, la
clase Monitor es propensa a que se produzcan bloqueos huérfanos e interbloqueos.
Clase Monitor
La clase Monitor proporciona una funcionalidad adicional, que se puede utilizar junto con la
instrucción lock:
lector se bloquean hasta que todos los lectores existentes salen del bloqueo y el sistema de
escritura entra y sale del bloqueo.
ReaderWriterLockSlim tiene afinidad de subprocesos.
Clase Semaphore
La clase Semaphore permite a un número especificado de subprocesos tener acceso a un
recurso. Los subprocesos adicionales que solicitan el recurso se bloquean hasta que un
subproceso libera el semáforo.
Como la clase Mutex, Semaphore deriva de WaitHandle. También, al igual que Mutex, un
Semaphore puede ser local o global. Se puede utilizar más allá de los límites del dominio de
aplicación.
A diferencia de Monitor, Mutex y ReaderWriterLock, Semaphore no tienen afinidad de
subprocesos. Esto significa se puede utilizar en escenarios en los que un subproceso adquiere el
semáforo y otro lo libera.
System.Threading.SemaphoreSlim es un semáforo ligero para sincronización dentro del límite
de un único proceso.
Señalización
La manera más sencilla de esperar una señal de otro subproceso es llamar al método Join, que se
bloquea hasta que se complete el otro subproceso. Join tiene dos sobrecargas que permiten al
subproceso bloqueado interrumpir la espera una vez transcurrido un intervalo especificado.
Los identificadores de espera proporcionan un conjunto mucho más rico de capacidades de
espera y señalización.
Controladores de espera
Los identificadores de espera derivan de la clase WaitHandle, que a su vez deriva de
MarshalByRef Object. Así, los identificadores de espera se pueden utilizar para sincronizar las
actividades de los subprocesos fuera de los límites del dominio de aplicación.
Los subprocesos se bloquean en los identificadores de espera llamando al método de instancia
WaitOne o uno de los métodos estáticos WaitAll, WaitAny o SignalAndWait. La forma de
liberación depende de qué método se llamó y del tipo de identificadores de espera.
Identificadores de espera de evento
Los identificadores de espera de evento incluyen la clase EventWaitHandle y sus clases
derivadas, AutoResetEvent y ManualResetEvent. Los subprocesos se liberan desde un
identificador de espera de eventos cuando el identificador de espera de eventos está señalado
llamando a su método Set o utilizando el método SignalAndWait.
Los identificadores de espera de eventos se pueden restablecer automáticamente, como un
torniquete que sólo permite una lectura cada vez que se señala, o se debe restablecer
manualmente, como una puerta que está cerrada hasta que se señala y luego queda abierta hasta
que alguien la cierra. Cuando sus nombres implican, AutoResetEvent y ManualResetEvent
representan el primer caso y el último, respectivamente.
System.Threading.ManualResetEventSlim es un evento ligero para sincronización dentro del
límite de un único proceso.
EventWaitHandle puede representar cualquier tipo de evento y puede ser local o global. Las
clases derivadas AutoResetEvent y ManualResetEvent siempre son locales.
Los identificadores de espera de evento no tienen afinidad de subprocesos. Cualquier
subproceso puede señalar un identificador de espera de evento.
namespace BarrierSimple
{
class Program
{
static string[] words1 = new string[] { "brown", "jumped", "the",
"fox",
"quick"};
static string[] words2 = new string[] { "dog", "lazy","the","over"};
static string solution = "the quick brown fox jumped over the lazy
dog.";
Barrier es un objeto que impide que las tareas individuales de una operación paralela continúen
hasta que todas las tareas alcancen la barrera. Es útil cuando una operación paralela tiene lugar
en fases y cada fase requiere sincronización entre las tareas. En este ejemplo hay dos fases en la
operación. En la primera fase, cada tarea rellena su sección del búfer con datos. Cuando cada
tarea termina de rellenar su sección, la tarea señaliza a la barrera que está lista para continuar y
espera. Cuando todas las tareas han señalizado la barrera, se desbloquean y comienza la segunda
fase. La barrera es necesaria porque la segunda fase requiere que cada tarea obtenga acceso a
todos los datos generados hasta este momento. Sin la barrera, las primeras tareas en completarse
podrían intentar leer de búferes que otras tareas no han rellenado todavía. Es posible sincronizar
cualquier número de fases de esta manera.
1.4.12. SpinLock
La estructura SpinLock es una primitiva de sincronización de exclusión mutua y bajo nivel que
itera en ciclos mientras espera adquirir un bloqueo. En equipos con varios núcleos, en que los
períodos de tiempo de espera deben ser breves y en que la contención es mínima, el
comportamiento de SpinLock puede ser mejor que el de otros tipos de bloqueos. Sin embargo,
se recomienda utilizar SpinLock solamente si se determina mediante la generación de perfiles
que los métodos System.Threading.Monitor o Interlocked están reduciendo el rendimiento del
programa de forma significativa.
SpinLock puede proporcionar el intervalo de tiempo del subproceso aunque no haya adquirido
el bloqueo todavía. El motivo es evitar la inversión de la prioridad del subproceso y permitir el
progreso del recolector de elementos no utilizados. Cuando se utiliza SpinLock, conviene
asegurarse de que ningún subproceso mantenga el bloqueo durante más de un brevísimo
intervalo de tiempo, y que ningún subproceso se pueda bloquear mientras mantiene el bloqueo.
Como SpinLock es un tipo de valor, se debe pasar explícitamente por referencia si se pretende
que las dos copias hagan referencia al mismo bloqueo.
SpinLock admite un modo de seguimiento de subprocesos que se puede utilizar durante la fase
de desarrollo para ayudar a realizar el seguimiento del subproceso que está manteniendo el
bloqueo en un momento concreto. El modo de seguimiento de subprocesos es muy útil para la
depuración, pero se recomienda desactivarlo en la versión de lanzamiento del programa porque
puede reducir el rendimiento.
1.4.12.1. Cómo: Utilizar SpinLock para la sincronización de bajo nivel
En el siguiente ejemplo se muestra cómo utilizar SpinLock.
Ejemplo
En este ejemplo, la sección crítica realiza una cantidad de trabajo mínima, lo que lo convierte en
un buen candidato para SpinLock. Al aumentar ligeramente el trabajo aumenta el rendimiento
de SpinLock en comparación con un bloqueo estándar. Sin embargo, hay un punto en el que un
bloqueo por subproceso es más caro que un bloqueo estándar. Se puede usar la nueva
funcionalidad de generación de perfiles de simultaneidad de las Herramientas de generación de
perfiles para ver qué tipo de bloqueo proporciona mayor rendimiento en su programa.
class SpinLockDemo2
{
const int N = 100000;
static Queue<Data> _queue = new Queue<Data>();
static object _lock = new Object();
static SpinLock _spinlock = new SpinLock();
class Data
{
public string Name { get; set; }
public double Number { get; set; }
}
{
UpdateWithLock(new Data() { Name = i.ToString(), Number =
i }, i);
}
}
);
sw.Stop();
Console.WriteLine("elapsed ms with lock: {0}",
sw.ElapsedMilliseconds);
}
}
using System.Threading.Tasks;
namespace SpinLockDemo
{
public class SpinLockTest
{
// Specify true to enable thread tracking. This will cause
// exception to be thrown when the first thread attempts to reenter
the lock.
// Specify false to cause deadlock due to coding error below.
private static SpinLock _spinLock = new SpinLock(true);
}
}
{
head = m_head;
node.Next = head;
if (Interlocked.CompareExchange(ref m_head, node, head) == head)
break;
spin.SpinOnce();
}
}
namespace CDS_Spinwait
{
class Latch
{
// 0 = unset, 1 = set
private volatile int m_state = 0;
private ManualResetEvent m_ev = new ManualResetEvent(false);
#if LOGGING
// For fast logging with minimal impact on latch behavior. Spin counts
#if LOGGING
spinCountLog[spinner.Count]++;
#endif
return true;
}
}
class Program
{
static Latch latch = new Latch();
static int count = 2;
static CancellationTokenSource cts = new CancellationTokenSource();
El bloqueo temporal utiliza el objeto SpinWait para girar en su posición solo hasta que la
llamada siguiente a SpinOnce haga que SpinWait genere el intervalo de tiempo del subproceso.
A partir de ese momento, el bloqueo temporal produce su propio cambio de contexto llamando a
WaitOne en ManualResetEvent y pasando el resto del valor del tiempo de espera.
El resultado del registro muestra con qué frecuencia Latch pudo aumentar el rendimiento
adquiriendo el bloqueo sin utilizar ManualResetEvent.
Para una visión completa de TAP, APM y EAP, consulte los vínculos proporcionados en la
siguiente sección.
2.1. Modelo asincrónico basado en tareas (TAP)
El modelo asincrónico basado en tareas (TAP) se basa en los tipos Task y Task<TResult> del
espacio de nombres System.Threading.Tasks, que se usan para representar operaciones
asincrónicas arbitrarias. TAP es el modelo asincrónico de diseño recomendado para el nuevo
desarrollo.
Nombres, parámetros y tipos de valores devueltos
TAP usa un solo método para representar el inicio y la finalización de una operación
asincrónica. Esto contrasta con el modelo de programación asincrónica (APM o IAsyncResult),
que necesita métodos Begin y End y, con el modelo asincrónico basado en eventos (EAP), que
necesita un método que tenga el sufijo Async y también necesita uno o más eventos, tipos de
delegado de controlador de eventos y tipos derivados de EventArg. Los métodos asincrónicos de
TAP incluyen el sufijo Async después del nombre de la operación; por ejemplo, GetAsync para
una operación Get. Si va a agregar un método de TAP a una clase que ya contiene ese nombre
de método con el sufijo Async, use el sufijo TaskAsync en su lugar. Por ejemplo, si la clase ya
tiene un método GetAsync, use el nombre GetTaskAsync.
El método de TAP devuelve Task o Task<TResult>, en función de si el método sincrónico
correspondiente devuelve void o un tipo TResult.
Los parámetros de un método de TAP deben coincidir con los parámetros de su homólogo
sincrónico y se deben proporcionar en el mismo orden. Sin embargo, los parámetros out y ref
están exentos de esta regla y se deben evitar completamente. En su lugar, los datos que se
hubieran devuelto con un parámetro out o ref se deben devolver como parte del tipo TResult
devuelto por Task<TResult> y deben usar una tupla o una estructura de datos personalizada para
incluir varios valores. Los métodos que están dedicados exclusivamente a la creación,
manipulación o combinación de tareas (donde el intento asincrónico del método está claro en el
nombre del método o en el nombre del tipo al que el método pertenece) no necesitan seguir este
modelo de nombres; esos métodos se conocen a menudo como combinadores (también
denominados elementos de combinación). Los ejemplos de combinadores incluyen WhenAll y
WhenAny, y se describen en la sección que describe los combinadores integrados basados en
tareas titulados Utilizar los elementos de combinación basados en tareas integradas del artículo
Utilizar el modelo asincrónico basado en tareas.
Iniciar una operación asincrónica
Un método asincrónico basado en TAP puede hacer una pequeña cantidad de trabajo
sincrónicamente, como validar argumentos e iniciar la operación asincrónica, antes de que
devuelva la tarea resultante. El trabajo sincrónico debe reducirse al mínimo de modo que el
método asincrónico pueda volver rápidamente. Entre las razones para un retorno rápido se
incluyen las siguientes:
Los métodos asincrónicos se pueden invocar desde subprocesos de la interfaz de usuario
(UI) y cualquier trabajo sincrónico de ejecución prolongada puede dañar la capacidad
de respuesta de la aplicación.
Se pueden iniciar varios métodos asincrónicos simultáneamente. Por tanto, cualquier
trabajo de ejecución prolongada en la parte sincrónica de un método asincrónico puede
retrasar el inicio de otras operaciones asincrónicas, lo que reduce las ventajas de la
simultaneidad.
En algunos casos, la cantidad de trabajo necesario para completar la operación es menor que la
cantidad de trabajo necesario para iniciar la operación de forma asincrónica. La lectura de una
secuencia donde la operación de lectura se puede satisfacer mediante datos que ya están
almacenados en búfer en la memoria es un ejemplo de este escenario. En casos como este, la
operación puede completarse sincrónicamente y puede devolver una tarea que ya se ha
completado.
Excepciones
Un método asincrónico debe generar una excepción fuera de la llamada de método asincrónico
solo como respuesta a un error de uso. Los errores de uso nunca deben producirse en código de
producción. Por ejemplo, si al pasar una referencia nula (Nothing en Visual Basic) como uno de
los argumentos del método se produce un estado de error (representado normalmente por una
excepción ArgumentNull Exception), puede modificar el código de llamada para asegurarse de
que nunca se pase una referencia nula. Para todos los demás errores, las excepciones que se
producen cuando se ejecuta un método asincrónico deben asignarse a la tarea devuelta, aunque
Si un método FindFilesAsync devuelve una lista de todos los archivos que reúnen un patrón
particular de búsqueda, la devolución de progreso puede proporcionar una estimación del
porcentaje de trabajo completado así como el conjunto de resultados parciales. Puede hacerlo
con una tupla:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(string pattern,
IProgress<Tuple<double,ReadOnlyCollection<List<FileInfo>>>> progress);
En este último caso, el tipo de datos especial suele tener el sufijo ProgressInfo.
Si las implementaciones de TAP proporcionan sobrecargas que aceptan un parámetro progress,
deben permitir que el argumento sea null, en cuyo caso no se notifica ningún progreso. Las
implementaciones de TAP deben notificar el progreso al objeto Progress<T> sincrónicamente,
lo cual permite al método asincrónico proporcionar rápidamente el progreso y hacer que el
consumidor del progreso determine cómo y dónde es mejor controlar la información. Por
ejemplo, la instancia de progreso puede elegir hacerse con las devoluciones de llamada y
provocar eventos en un contexto capturado de sincronización.
Implementaciones IProgress<t>
.NET Framework 4.5 proporciona una única implementación de IProgress<T>: Progress<T>. Se
declara la clase Progress<T> como se indica a continuación:
public class Progress<T> : IProgress<T>
{
public Progress();
public Progress(Action<T> handler);
protected virtual void OnReport(T value);
public event EventHandler<T> ProgressChanged;
}
Una instancia de Progress<T> expone un evento ProgressChanged, que se provoca cada vez que
la operación asincrónica informe de una actualización de progreso. El evento ProgressChanged
se genera en el objeto SynchronizationContext que se capturó cuando se creó la instancia de
Progress<T>. Si no había ningún contexto de sincronización disponible, se usa un contexto
predeterminado destinado al grupo de subprocesos. Los controladores pueden registrarse con
este evento. Un único controlador también puede ser proporcionado al constructor Progress<T>
por comodidad y se comporta como un controlador de eventos para el evento ProgressChanged.
Las actualizaciones de progreso se generan de forma asincrónica para evitar retrasar la
operación asincrónica mientras los controladores de eventos se ejecutan. Otra implementación
IProgress<T> podría elegir aplicarse para diferentes semánticas.
Elegir las sobrecargas que se van a proporcionar
Si una implementación TAP utiliza CancellationToken opcional y los parámetros opcionales
IProgress <T>, podría requerir hasta cuatro sobrecargas:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…, CancellationToken cancellationToken,
IProgress<T> progress);
Si una implementación TAP admite cancelación y progreso, puede exponer las cuatro
sobrecargas. Sin embargo, puede proporcionar sólo los dos siguientes:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);
Para compensar las dos combinaciones intermedias que faltan, los desarrolladores pueden pasar
None o un CancellationToken predeterminado para el parámetro cancellationToken y null para
el parámetro progress.
Si se espera que cada uso del método TAP admita cancelación o progreso, puede omitir las
sobrecargas que no acepten el parámetro pertinente.
Si se decide exponer varias sobrecargas para crear la cancelación o para el progreso opcional,
las sobrecargas que no admitan cancelación o progreso deben comportarse como si pasaran
None para cancelación o null para el progreso en la sobrecarga que admite ambas.
2.1.1. Implementar el modelo asincrónico basado en tareas
Puede implementar el modelo asincrónico basado en tareas (TAP) de tres maneras: mediante los
compiladores de C# y Visual Basic en Visual Studio, manualmente o mediante una combinación
del compilador y métodos manuales. En las siguientes secciones se describe cada método con
detalle. Puede usar el modelo de TAP para implementar operaciones asincrónicas enlazadas a
cálculos y enlazadas a E/S; en la sección Cargas de trabajo se describe cada tipo de operación.
Generar métodos de TAP
Usar los compiladores
En Visual Studio 2012 y .NET Framework 4.5, cualquier método que tenga la palabra clave
async (Async en Visual Basic) se considera un método asincrónico, y los compiladores de C# y
Visual Basic realizan las transformaciones necesarias para implementar el método de forma
asincrónica mediante TAP. Un método asincrónico debe devolver un objeto Task o
Task<TResult>. En el último caso, el cuerpo de la función debe devolver TResult y el
compilador asegura que este resultado está disponible a través del objeto de la tarea resultante.
Del mismo modo, se calculan en la tarea de salida las referencias de cualquier excepción no
controlada dentro del cuerpo del método y esto hace que la tarea resultante finalice en el estado
Faulted. La excepción es cuando un objeto OperationCanceledException (o un tipo derivado) no
está controlado, en cuyo caso la tarea resultante finaliza en el estado Canceled.
Generar métodos de TAP manualmente
Puede implementar el modelo de TAP manualmente para tener un mejor control sobre la
implementación. El compilador se basa en la superficie pública expuesta del espacio de nombres
System.Threading.Tasks y los tipos auxiliares del espacio de nombres
System.Runtime.CompilerServices. Para implementar TAP personalmente, cree un objeto
TaskCompletionSource<TResult>, realice la operación asincrónica y, cuando se complete,
llame al método SetResult, SetException o SetCanceled, o a la versión Try de uno de estos
métodos. Cuando implementa un método de TAP manualmente, debe completar la tarea
resultante cuando la operación asincrónica representada se complete. Por ejemplo:
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset
, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
Enfoque híbrido
Puede resultar útil implementar el modelo de TAP manualmente pero delegar la lógica básica de
la implementación en el compilador. Por ejemplo, quizás desee usar el enfoque híbrido cuando
desee comprobar argumentos fuera de un método asincrónico generado por el compilador de
forma que las excepciones puedan salir del llamador directo del método en lugar de exponerse a
través del objeto Task:
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
Otro caso donde es útil esa delegación es cuando implementa la optimización de acceso rápido y
desea devolver una tarea almacenada en memoria caché.
Cargas de trabajo
Puede implementar operaciones asincrónicas enlazadas a cálculos y enlazadas a E/S como
métodos de TAP. Sin embargo, cuando los métodos de TAP se exponen públicamente desde una
biblioteca, solo se deben suministrar para cargas de trabajo que impliquen operaciones
enlazadas a E/S (también pueden implican cálculos, pero no deben ser estrictamente de cálculo).
Si un método está enlazado a cálculos puramente, solo se debe exponer como una
implementación sincrónica; el código que lo usa puede elegir si ajustar una invocación de ese
método sincrónico en una tarea para descargar el trabajo en otro subproceso o para lograr el
paralelismo.
Tareas enlazadas a cálculos
La clase Task es idónea para representar operaciones intensivas en cálculos. De forma
predeterminada, se beneficia de la compatibilidad especial dentro de la clase ThreadPool para
proporcionar una ejecución eficaz, y también proporciona un buen control sobre cuándo, dónde
y cómo se ejecutan los cálculos asincrónicos. Puede generar tareas enlazadas a cálculos de las
maneras siguientes:
En .NET Framework 4, use el método TaskFactory.StartNew, que acepta un delegado
(normalmente Action<T> o Func<TResult>) que se va a ejecutar de forma asincrónica.
Si proporciona un delegado de Action<T>, el método devuelve un objeto Task que
representa la ejecución asincrónica de ese delegado. Si proporciona un delegado de
Func<TResult>, el método devuelve un objeto Task<TResult>. Las sobrecargas del
método StartNew aceptan un token de cancelación (CancellationToken), las opciones de
creación de la tarea (TaskCreationOptions) y un programador de tareas
(TaskScheduler), todo lo cual proporciona un control específico sobre la programación
y la ejecución de la tarea. Una instancia de generador que tiene como destino el
programador de tareas actual está disponible como una propiedad estática (Factory) de
la clase Task; por ejemplo: Task.Factory.StartNew(…).
En .NET Framework 4.5, use el método estático Task.Run como acceso directo a
TaskFactory.StartNew. Puede usar Run para iniciar fácilmente una tarea enlazada a
cálculos destinada al grupo de subprocesos. En .NET Framework 4.5, este es el
mecanismo preferido para iniciar una tarea enlazada a cálculos. Use StartNew
directamente solo cuando desee un mayor control sobre la tarea.
Use los constructores del tipo Task o el método Start si desea generar y programar la
tarea por separado. Los métodos públicos solo deben devolver tareas que ya se han
iniciado.
Use las sobrecargas del método Task.ContinueWith. Este método crea una nueva tarea
que se programa cuando se completa otra tarea. Algunas de las sobrecargas de
ContinueWith aceptan un token de cancelación, opciones de continuación y un
programador de tareas para tener un mejor control sobre la programación y la ejecución
de la tarea de continuación.
Las tareas enlazadas a cálculos finalizan en un estado Canceled si se cumple al menos una de las
condiciones siguientes:
Llega una solicitud de cancelación a través del objeto CancellationToken, que se
proporciona como argumento al método de creación (por ejemplo, StartNew o Run)
antes de que la tarea cambie al estado Running.
Una excepción OperationCanceledException no está controlada dentro del cuerpo de
esta tarea, esa excepción contiene el mismo objeto CancellationToken que se pasa a la
tarea y ese token muestra que se solicitó la cancelación.
Si hay otra excepción no controlada en el cuerpo de la tarea, la tarea finaliza en el estado
Faulted y cualquier intento de esperar en la tarea u obtener acceso a su resultado produce una
excepción.
Tareas enlazadas a E/S
Para crear una tarea que no se deba respaldar directamente por un subproceso durante toda su
ejecución, use el tipo TaskCompletionSource<TResult>. Este tipo expone una propiedad Task
que devuelve una instancia asociada de Task<TResult>. El ciclo de vida de esta tarea se
controla mediante métodos TaskCompletionSource<TResult> como SetResult, SetException,
SetCanceled y sus variantes de TrySet.
Suponga que desea crear una tarea que se completará después de un período de tiempo
especificado. Por ejemplo, puede que desee retrasar una actividad en la interfaz de usuario. La
clase System.Threading.Timer ya proporciona la capacidad de invocar de forma asincrónica un
delegado después de un período de tiempo especificado, y mediante
TaskCompletionSource<TResult> puede colocar un objeto Task<TResult> delante del
temporizador, por ejemplo:
A partir de .NET Framework 4.5, el método Task.Delay se proporciona con este propósito y
puede usarlo dentro de otro método asincrónico, por ejemplo, para implementar un bucle
asincrónico de sondeo:
public static async Task Poll(Uri url, CancellationToken cancellationToken,
IProgress<bool> progress)
{
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}
basada en imageData de entrada. Este imageData podría proceder de un servicio Web al que
tiene acceso de forma asincrónica:
public async Task<Bitmap> DownloadDataAndRenderImageAsync(
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
En este ejemplo también se muestra cómo se puede incluir en un subproceso un único token de
cancelación mediante varias operaciones asincrónicas.
2.1.2. Utilizar el modelo asincrónico basado en tareas
Cuando se utiliza el patrón asincrónico basado en tareas (TAP) para ejecutar operaciones
asincrónicas, se pueden utilizar devoluciones de llamada para conseguir esperas sin bloqueos.
Para las tareas, esto se logra con métodos como Task.ContinueWith. La compatibilidad
asincrónica basada en lenguajes oculta devoluciones de llamada al permitir que las operaciones
asincrónicas sean esperadas dentro del flujo de control normal y el código generado por el
compilador proporcione esta misma compatibilidad a nivel de API.
Suspender la ejecución con Await
Empezando con .NET Framework 4.5, se puede utilizar la palabra clave await (Referencia de
C#) en C# y Await (Operador) (Visual Basic) en Visual Basic para esperar asincrónicamente a
Task y a los objetos Task<TResult>. Cuando se está esperando una Task, la expresión await es
de tipo void. Cuando se está esperando una Task<TResult>, la expresión await es de tipo
TResult. Una expresión await debe aparecer en el cuerpo de un método asincrónico. Para
obtener más información sobre la compatibilidad del lenguaje C# y Visual Basic en .NET
Framework 4.5, consulte las especificaciones del lenguaje C# y Visual Basic.
Más en detalle, la función de espera instala una devolución de llamada en la tarea mediante una
continuación. Esta devolución de llamada reanuda el método asincrónico en el momento de
suspensión. Cuando se reanuda el método asincrónico, si la operación en espera se completa
correctamente y fue Task<TResult>, se devuelve su TResult. Si Task o Task<TResult>
esperado terminó en el estado Canceled, se lanza una excepción OperationCanceledException.
Si Task o Task<TResult> esperado terminó en el estado Faulted, se lanza la excepción que
produjo el error. Task puede darse como resultado de varias excepciones, pero sólo una de estas
excepciones se propaga. Sin embargo, la propiedad Task.Exception devuelve una excepción
AggregateException que contiene todos los errores.
Si un contexto de sincronización (objetoSynchronizationContext) está asociado al subproceso
que estaba ejecutando el método asincrónico en el momento de la suspensión (por ejemplo, si la
propiedad SynchronizationContext.Current no es null), el método asincrónico se reanuda en el
mismo contexto de sincronización utilizando el método Post del contexto. De lo contrario, se
basa en el programador de tareas (objetoTaskScheduler) que estaba en curso en el momento de
la suspensión. Normalmente, éste es el programador de tareas predeterminado
(TaskScheduler.Default), que tiene como destino el grupo de subprocesos. Este programador de
tareas determina si la operación asincrónica esperada debe reanudarse donde se completó o si la
reanudación debe programarse. El programador predeterminado suele permitir que la
continuación se ejecute en el subproceso que la operación en espera completó.
Cuando se invoca un método asincrónico, se ejecuta sincrónicamente el cuerpo de la función
hasta la primera expresión en espera de una instancia esperable que aún no se ha completado,
punto en el que la invocación vuelve al llamador. Si el método asincrónico no devuelve void, se
devuelve un objeto Task o Task<TResult> para representar el cálculo en curso. En un método
asincrónico que no es void, si se encuentra una instrucción de retorno, o se alcanza el final del
cuerpo del método, la tarea se completa en el estado final RanToCompletion. Si una excepción
no controlada hace que el control deje el cuerpo del método asincrónico, la tarea termina en el
Para cancelar llamadas asincrónicas múltiples, se puede pasar el mismo token a todas las
invocaciones:
var cts = new CancellationTokenSource();
Task.WhenAll
Utilice el método WhenAll de forma asincrónica para atender varias operaciones asincrónicas
que se representan como tareas. El método tiene múltiples sobrecargas que admiten un conjunto
de tareas no genéricas o un conjunto no uniforme de tareas genéricas (por ejemplo, tareas que
esperan asincrónicamente varias operaciones de retorno tipo void, o tareas que esperan
asincrónicamente múltiples métodos de valores de retorno en los que cada valor puede ser de
diferente tipo) así como también admiten un conjunto uniforme de tareas genéricas (tales como
esperar asincrónicamente varios métodos de retorno TResult).
Digamos que se desea enviar un correo electrónico a varios clientes Se puede solapar el envío
de mensajes para que no se tenga que esperar a que un mensaje se complete antes de enviar el
siguiente. También se puede averiguar qué operaciones de envío se han completado y si se ha
producido algún error.
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
Este código no controla explícitamente las excepciones que se pueden producir, sino que
permite que las excepciones se propaguen fuera de await en la tarea resultante de WhenAll. Para
controlar las excepciones, se puede usar el siguiente código:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
...
}
En este caso, si una operación asincrónica da error, todas las excepciones se consolidarán en una
excepción AggregateException, que se almacena en Task que se devuelve desde el método
WhenAll. Sin embargo, sólo una de esas excepciones se propaga por la palabra clave await . Si
se desean examinar todas las excepciones, se puede volver a escribir el código anterior como
sigue:
Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
Considere el caso de cómo descargar varios archivos de la web de forma asincrónica. En este
caso, todas las operaciones asincrónicas tienen tipos de resultado homogéneos y el acceso a los
resultados es simple:
string [] pages = await Task.WhenAll(from url in urls select
DownloadStringAsync(url));
Como en el caso anterior que se devuelve void, las mismas técnicas de control de excepciones
se pueden utilizar aquí:
Task [] asyncOps = (from url in urls select
DownloadStringAsync(url)).ToArray();
try
{
string [] pages = await Task.WhenAll(asyncOps);
...
}
catch(Exception exc)
{
foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
Task.WhenAny
Se puede usar el método WhenAny para esperar de forma asincrónica a una de las múltiples
operaciones asincrónicas representadas como tareas a completar. Este método tiene cuatro usos
principales:
Redundancia: Efectuar múltiples veces una operación y seleccionar la que se complete
primero (por ejemplo: conectar con múltiples servicios web de cotización de valores
que producirán un solo resultado y seleccionar el que se complete más rápidamente).
Intercalado: Iniciar múltiples operaciones y esperar a que todas se completen, pero
procesarlas a medida que se van completando.
Regulación: Permitir que las operaciones adicionales comiencen cuando las otras se
completen. Esto es una extensión del escenario de intercalado.
Rescate anticipado: Por ejemplo, una operación representada por la tarea t1 se puede
agrupar en una tarea WhenAny con otra tarea t2 y se puede esperar la tarea WhenAny.
A diferencia de WhenAll, que devuelve los resultados empaquetados de todas las tareas que se
completan correctamente, WhenAny devuelve la tarea que se completó. Si una tarea da error, es
importante saber dónde está el error, y si acaba correctamente, es importante saber a qué tarea
está asociado el valor devuelto. Por consiguiente, es necesario tener acceso al resultado de la
tarea devuelta, o esperar aún más, como se muestra en este ejemplo.
Al igual que con WhenAll, se necesita poder alojar excepciones. Debido a que se recibe de
vuelta la tarea completada, se puede esperar que la tarea devuelta tenga los errores propagados,
y hacer try/catch sobre estos correctamente; por ejemplo:
Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
Task<bool> recommendation = await Task.WhenAny(recommendations);
try
{
if (await recommendation) BuyStock(symbol);
break;
}
catch(WebException exc)
{
recommendations.Remove(recommendation);
}
}
Además, aún en el caso de que una primera tarea se complete correctamente, las tareas
posteriores pueden producir errores. En este punto, son varias las opciones para abordar las
excepciones: se puede esperar a que se completen todas las tareas lanzadas, en cuyo caso se
puede usar el método WhenAll; o bien, se puede decidir que todas las excepciones son
importantes y se deben registrar. Para ello, se pueden utilizar directamente las continuaciones
para recibir una notificación cuando las tareas estén completadas de forma asincrónica:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}
O bien
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
o incluso:
Intercalado
Considere el caso en el que se descarguen imágenes de la web y se procese cada imagen (por
ejemplo, agregar la imagen a un control de UI). Se tiene que hacer el procesado secuencialmente
en el subproceso de UI, pero se desea que las imágenes se descarguen de forma simultánea en la
medida de lo posible. Además, no se desea esperar a agregar las imágenes a la UI hasta que se
hayan descargado todas (se desean agregar a medida que se completan):
List<Task<Bitmap>> imageTasks = (from imageUrl in urls select
GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}
Limitación de peticiones
Piense en el ejemplo de intercalado, excepto que el usuario ahora está descargando tantas
imágenes que las descargas necesitan que se limiten explícitamente; por ejemplo, sólo se desea
realizar un número específico de descargas simultáneamente. Para lograr esto, se puede iniciar
un subconjunto de las operaciones asincrónicas. A medida que las operaciones se completan, se
pueden iniciar operaciones adicionales para reemplazarlas:
const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch(Exception exc) { Log(exc); }
if (nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
}
Rescate temprano
Considere que se está esperando asincrónicamente que una operación se complete mientras
simultáneamente se responde a la solicitud de cancelación de un usuario (por ejemplo, un
usuario hace clic en el botón de cancelación). En el siguiente código se muestra este escenario:
private CancellationTokenSource m_cts;
Esta implementación vuelve a habilitar la interfaz de usuario en cuanto decidamos salir pero no
cancela las operaciones asincrónicas subyacentes. Otra alternativa sería cancelar las operaciones
pendientes cuando decidamos salir, aunque no se restaure la interfaz de usuario hasta que las
operaciones se completen efectivamente, posiblemente por una finalización temprana debido a
una solicitud de cancelación:
private CancellationTokenSource m_cts;
Otro ejemplo de rescate temprano implica usar el método WhenAny junto con el método Delay
como se discute en la siguiente sección.
Task.Delay
Se puede usar el método Delay para introducir pausas en una ejecución asincrónica del método.
Esto es útil para muchos tipos de funcionalidad, como compilar los bucles de sondeo y retrasar
el control de datos proporcionados por el usuario durante un período de tiempo predeterminado.
El método Delay puede también resultar útil junto con WhenAny para implementar tiempos de
espera.
Si una tarea que forma parte de una operación asincrónica mayor (por ejemplo, un servicio web
ASP.NET) tarda demasiado tiempo en completarse, la operación global podría sufrir,
especialmente si no llega a completarse nunca. Por esta razón, es importante poder tener
tiempos muertos cuando se espera en una operación asincrónica. Los métodos sincrónicos Wait,
WaitAll y WaitAny aceptan valores de tiempo de espera, pero los métodos
ContinueWhenAll/WhenAny y el mencionado previamente WhenAll/ WhenAny
correspondientes no. En su lugar, se puede usar Delay y WhenAny en conjunto para
implementar un tiempo de espera.
Por ejemplo, en la aplicación de interfaz de usuario, digamos que se desea descargar una imagen
y deshabilitar la interfaz de usuario mientras se descarga dicha imagen. Sin embargo, si la
descarga lleva mucho tiempo, se desea rehabilitar la interfaz de usuario y la descarga debe
descartarse:
public async void btnDownload_Click(object sender, EventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap> download = GetBitmapAsync(url);
if (download == await Task.WhenAny(download, Task.Delay(3000)))
{
Bitmap bmp = await download;
pictureBox.Image = bmp;
status.Text = “Downloaded”;
}
else
{
pictureBox.Image = null;
Lo mismo ocurre con las descargas múltiples, puesto que WhenAll devuelve una tarea:
public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap[]> downloads =
Task.WhenAll(from url in urls select GetBitmapAsync(url));
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
{
foreach(var bmp in downloads) panel.AddImage(bmp);
status.Text = “Downloaded”;
}
else
{
status.Text = “Timed out”;
downloads.ContinueWith(t => Log(t));
}
}
finally { btnDownload.Enabled = true; }
}
Se puede compilar un método auxiliar casi idéntico para las operaciones asincrónicas
implementadas con el TAP y que devuelvan tareas:
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }
}
return default(T);
}
Se puede usar este combinador para encapsular reintentos en la lógica de la aplicación; por
ejemplo:
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringAsync(url), 3);
Se podría extender la función RetryOnFault más adelante: Por ejemplo, la función podría
aceptar otro Func<Task> que se invocará entre reintentos para determinar cuándo probar la
operación otra vez; por ejemplo:
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }
await retryWhen().ConfigureAwait(false);
}
return default(T);
}
Se podría usar la función como se muestra para esperar un segundo antes de reintentar la
operación:
// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
() => DownloadStringAsync(url), 3, () => Task.Delay(1000));
NeedOnlyOne
A veces, se puede aprovechar la redundancia para mejorar la latencia y las posibilidades de
éxito de una operación. Piense en los múltiples servicios web que proporcionan cotizaciones,
pero que, durante varias horas del día cada uno de los servicios puede proporcionar diferentes
niveles de calidad y de tiempos de respuesta. Para tratar con estas fluctuaciones, se pueden
mandar solicitudes a todos los servicios web y tan pronto como se reciba una respuesta de una,
cancelar las solicitudes restantes. Se puede implementar una función auxiliar para facilitar la
implementación de este patrón común para iniciar múltiples operaciones, esperar alguna y
después de cancelar el resto: La función NeedOnlyOne en el siguiente ejemplo muestra este
escenario:
public static async Task<T> NeedOnlyOne(
params Func<CancellationToken,Task<T>> [] functions)
{
var cts = new CancellationTokenSource();
var tasks = (from function in functions
select function(cts.Token)).ToArray();
var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
cts.Cancel();
foreach(var task in tasks)
{
var ignored = task.ContinueWith(
t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
}
return completed;
}
Operaciones de intercalado
Existe un potencial problema de rendimiento si se usa el método WhenAny para admitir un
escenario de intercalado cuando se utilizan conjuntos de tareas muy grandes. Cada llamada a
WhenAny da como resultado una continuación que se registra con cada tarea. Para un número N
de tareas, esto da como resultado O(N2) continuaciones creadas sobre el tiempo de vida de la
operación de intercalado. Si se está trabajando con un gran número de tareas, se puede usar un
combinador (Interleaved en el siguiente ejemplo) para solucionar el problema de rendimiento:
static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
var inputTasks = tasks.ToList();
var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
select new TaskCompletionSource<T>()).ToList();
int nextTaskIndex = -1;
foreach (var inputTask in inputTasks)
{
inputTask.ContinueWith(completed =>
{
var source = sources[Interlocked.Increment(ref nextTaskIndex)];
if (completed.IsFaulted)
source.TrySetException(completed.Exception.InnerExceptions);
else if (completed.IsCanceled)
source.TrySetCanceled();
else
source.TrySetResult(completed.Result);
}, CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
return from source in sources
select source.Task;
}
Se podría utilizar el combinador para procesar los resultados de tareas a medida que éstas se
completan; por ejemplo:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
En ciertos escenarios de dispersión/recolección, quizás se desee esperar a todas las tareas de un
conjunto, a menos que una de ellas dé error, en cuyo caso se desee detener la espera tan pronto
se produzca la excepción. Se puede conseguir esto con un método combinador como
WhenAllOrFirstException en el siguiente ejemplo:
public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
var inputs = tasks.ToList();
var ce = new CountdownEvent(inputs.Count);
var tcs = new TaskCompletionSource<T[]>();
Action<Task> onCompleted = (Task completed) =>
{
if (completed.IsFaulted)
tcs.TrySetException(completed.Exception.InnerExceptions);
if (ce.Signal() && !tcs.Task.IsCompleted)
tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
};
foreach (var t in inputs) t.ContinueWith(onCompleted);
return tcs.Task;
}
Se puede usar esta memoria caché en métodos asincrónicos siempre que se necesiten los
contenidos de una página web. La clase AsyncCache garantiza que se estén descargando tan
pocas páginas como sea posible y guarda en memoria caché los resultados.
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtContents.Text = await m_webPages["http://www.microsoft.com"];
}
finally { btnDownload.IsEnabled = true; }
}
AsyncProducerConsumerCollection
También se pueden utilizar tareas para compilar las estructuras de datos con el fin de coordinar
las actividades asincrónicas entre sí. Piense en uno de los patrones paralelos clásicos de diseño:
productor/consumidor. En este patrón, los productores generan datos que los consumidores
adquieren y los productores y consumidores pueden ejecutarlos en paralelo. Por ejemplo, el
consumidor procesa el elemento 1, que ha sido generado previamente por un productor que está
ahora produciendo el elemento 2. Para el patrón productor/consumidor, siempre se necesita
alguna estructura de datos para almacenar el trabajo creado por los productores de forma que se
puedan notificar los nuevos datos a los consumidores y éstos puedan encontrarlos cuándo estén
disponibles.
A continuación se muestra una simple estructura de datos compilada sobre una tarea que habilita
los métodos asincrónicos que se utilizarán como productores y consumidores:
public class AsyncProducerConsumerCollection<T>
{
private readonly Queue<T> m_collection = new Queue<T>();
private readonly Queue<TaskCompletionSource<T>> m_waiting =
new Queue<TaskCompletionSource<T>>();
Con esa estructura de datos en su lugar, se puede escribir código como el siguiente:
private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.Take();
ProcessNextItem(nextItem);
}
}
…
private static void Produce(int data)
{
m_data.Add(data);
}
Puede usar el método FromAsync para implementar un contenedor de TAP para este método
como sigue:
public static Task<int> ReadAsync(
this Stream stream, byte [] buffer, int offset, int count)
{
if (stream == null) throw new ArgumentNullException(“stream”);
De TAP a APM
Si la infraestructura existente espera el modelo APM, también deseará realizar una
implementación de TAP y usarla donde se espere una implementación de APM. Como las tareas
se pueden componer y la clase Task implementa IAsyncResult, puede usar una función auxiliar
sencilla para ello. En el código siguiente se usa una extensión de la clase Task<TResult>, pero
puede usar una función casi idéntica para las tareas no genéricas.
public static IAsyncResult AsApm<T>(
this Task<T> task, AsyncCallback callback, object state)
{
if (task == null) throw new ArgumentNullException(“task”);
var tcs = new TaskCompletionSource<T>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions)
else if (t.IsCanceled) tcs.TrySetCanceled();
else tcs.TrySetResult(t.Result);
if (callback != null) callback(tcs.Task);
}, TaskScheduler.Default);
return tcs.Task;
}
Con este método se pueden usar las implementaciones existentes WaitHandle en métodos
asincrónicos. Por ejemplo, si desea restringir el número de operaciones asincrónicas que se
ejecutan en un momento dado, puede usar un semáforo (objeto System.Threading.Semaphore).
Se puede restringir a N el número de operaciones que se ejecutan en paralelo, inicializando el
recuento del semáforo a N, esperando al semáforo cuando se desee realizar una operación y
liberando el semáforo cuando se haya terminado:
static Semaphore m_throttle = new Semaphore(N, N);
La clase ficticia AsyncExample tiene dos métodos y ambos admiten invocaciones sincrónicas y
asincrónicas. Las sobrecargas sincrónicas se comportan como cualquier llamada a un método y
ejecutan la operación en el subproceso que realiza la llamada. Si esta operación llevase mucho
tiempo, podría haber un retraso significativo en la devolución de la llamada. Las sobrecargas
asincrónicas inician la operación en otro subproceso y después regresan inmediatamente, lo que
permite que el subproceso que realiza la llamada continúe mientras la operación se ejecuta "en
segundo plano".
Sobrecargas de método asincrónicas
Hay dos sobrecargas posibles para las operaciones asincrónicas: de invocación única y de
invocación múltiple. Las dos formas se distinguen por sus signaturas de método: la sobrecarga
de invocación múltiple tiene un parámetro adicional denominado userState. Esto permite que el
código llame varias veces al método Method1Async(string param, object userState), sin esperar
a que finalice ninguna operación asincrónica pendiente. Si, por otro lado, intenta llamar al
método Method1Async(string param) antes de que haya finalizado una invocación anterior, el
método producirá una excepción InvalidOperationException.
El parámetro userState para las sobrecargas de invocación múltiple le permite distinguir entre
operaciones asincrónicas. Debe proporcionar un valor único, como un identificador único global
(GUID) o código hash, para cada llamada al método Method1Async(string param, object
userState) y, una vez finalizada cada operación, el controlador de eventos podrá determinar qué
instancia de la operación provocó el evento de finalización.
Realizar el seguimiento de las operaciones pendientes
Si utiliza las sobrecargas de invocación múltiple, el código necesitará realizar un seguimiento de
los objetos userState (identificadores de tarea) para las tareas pendientes. Para cada llamada al
método Method1Async(string param, object userState), normalmente generará un nuevo y único
objeto userState y lo agregará a una colección. Cuando la tarea correspondiente a este objeto
userState provoque el evento de finalización, la implementación del método de finalización
examinará AsyncCompletedEventArgs. UserState, que después se quitará de la colección.
Utilizado de esta manera, el parámetro userState asume el rol de un identificador de tarea.
Nota
Debe tener cuidado de proporcionar un valor único para userState en las llamadas a sobrecargas
de invocación múltiple. Los identificadores de tarea con un valor no único harán que la clase
asincrónica produzca una excepción ArgumentException.
Cancelar las operaciones pendientes
Es importante poder cancelar las operaciones asincrónicas en cualquier momento antes de su
finalización. Las clases que implementen el modelo asincrónico basado en eventos tendrán un
método CancelAsync (si solo hay un método asincrónico) o un método
nombreDeMétodoAsyncCancel (si hay varios métodos asincrónicos).
Los métodos que permiten varias invocaciones toman un parámetro userState, que se puede usar
para realizar un seguimiento de la duración de cada tarea. CancelAsync toma un parámetro
userState, que le permite cancelar determinadas tareas pendientes.
Los métodos que admiten sólo una operación pendiente cada vez, como Method1Async(string
param), no son cancelables.
Recibir actualizaciones del progreso y resultados incrementales
Toda clase que se ajuste al modelo asincrónico basado en eventos puede proporcionar
opcionalmente un evento para realizar el seguimiento del progreso y los resultados
incrementales. Dicho evento se suele denominar ProgressChanged o
nombreDeMétodoProgressChanged y su controlador de eventos correspondiente tomará un
parámetro ProgressChangedEventArgs.
El controlador de eventos para el evento ProgressChanged puede examinar la propiedad
ProgressChangedEventArgs.ProgressPercentage para determinar qué porcentaje de una tarea
asincrónica ha finalizado. Esta propiedad oscilará entre 0 y 100 y se puede utilizar para
actualizar la propiedad Value de un objeto ProgressBar. Si hay varias operaciones asincrónicas
Nota
Es perfectamente aceptable, siempre que sea factible y adecuado, reutilizar
tipos AsyncCompletedEventArgs y de delegado. En este caso, los nombres
asignados no serán necesariamente coherentes con el nombre del método, ya
que ni el delegado ni AsyncCompletedEventArgs estarán asociados a un único
método.
Admisión opcional de la cancelación
Si una clase va a admitir la cancelación de operaciones asincrónicas, dicha cancelación se
debería exponer al cliente tal y como se describe a continuación. Observe que deben resolverse
dos cuestiones antes de definir si se admite la cancelación:
¿Tiene la clase, incluida cualquier futura adición que se pueda anticipar, una única
operación asincrónica que admita la cancelación?
¿Son compatibles las operaciones asincrónicas que admiten la cancelación con la
existencia de varias operaciones pendientes? Es decir, ¿puede tomar el método
nombreDeMétodoAsync un parámetro userState y permitir varias invocaciones sin
necesidad de esperar a que finalice cualquiera de ellas?
Busque las respuestas a estas dos preguntas en la tabla siguiente para determinar cuál debería ser
la signatura para el método de cancelación.
Admisión de varias operaciones
Sólo una operación cada vez
simultáneas
Una operación void void
Aunque en general no se recomienda el uso de out y ref en .NET Framework, he aquí las reglas
que se deben seguir cuando están presentes:
Dado un método sincrónico nombreDeMétodo:
Los parámetros out del método nombreDeMétodo no deberían formar parte de
nombreDeMétodoAsync. En su lugar, deberían formar parte de
nombreDeMétodoCompleted EventArgs con el mismo nombre que su parámetro
equivalente en nombreDeMétodo (a menos que haya un nombre más adecuado).
Los parámetros ref del método nombreDeMétodo deberían aparecer como parte de
nombreDeMétodoAsync, y también como parte de
nombreDeMétodoCompletedEventArgs con el mismo nombre que su parámetro
equivalente en nombreDeMétodo (a menos que haya un nombre más adecuado).
Por ejemplo, dado el código:
public int MethodName(string arg1, ref string arg2, out string arg3);
// Bad design
private void Form1_MethodNameCompleted(object sender,
MethodNameCompletedEventArgs e)
{
DemoType result = (DemoType)(e.Result);
}
No defina una clase EventArgs para devolver los métodos que devuelven void. En su
lugar, use una instancia de la clase AsyncCompletedEventArgs.
Asegúrese de que se genere siempre el evento NombreMétodoCompleted. Este evento
se debe generar cuando se complete correctamente, cuando se produzca un error o
cuando se cancele. Nunca debería ocurrir que una aplicación permaneciese inactiva sin
llegar nunca a la finalización.
Asegúrese de que se detecte cualquier excepción que se produzca en la operación
asincrónica y se asigne la excepción detectada a la propiedad Error.
Si se produjo un error al completar la tarea, los resultados no deben ser accesibles.
Cuando la propiedad Error no es null, asegúrese de que el acceso a cualquier propiedad
de la estructura EventArgs genere una excepción. Utilice el método
RaiseExceptionIfNecessary para realizar la comprobación anterior.
Modele un tiempo de espera como un error. Cuando se produzca un tiempo de espera,
genere el evento NombreMétodoCompleted y asigne TimeoutException a la propiedad
Error.
Si la clase admite varias invocaciones simultáneas, asegúrese de que el evento
NombreMétodoCompleted contenga el objeto userSuppliedState apropiado.
Asegúrese de que el evento NombreMétodoCompleted se genere en el subproceso
adecuado y en el momento oportuno del ciclo de vida de la aplicación.
Operaciones que se ejecutan simultáneamente
Si la clase admite varias invocaciones simultáneas, deje que el programador haga un
seguimiento independiente de cada invocación definiendo la sobrecarga
MethodNameAsync que toma un parámetro de estado con valor de objeto, o un
identificador de tarea, denominado userSuppliedState. Dicho parámetro siempre debería
ser el último parámetro en la firma del método MethodNameAsync.
Si la clase define la sobrecarga MethodNameAsync que toma un parámetro de estado
con valor de objeto, o un identificador de tarea, asegúrese de realizar el seguimiento de
la duración de la operación con ese identificador de tarea y de devolverlo al controlador
de finalización. Hay clases de ayuda disponibles para asistir este proceso.
Si la clase define el método MethodNameAsync sin el parámetro de estado y no admite
varias invocaciones simultáneas, asegúrese de que cualquier intento de invocar
MethodNameAsync, antes de que haya finalizado la invocación previa de
MethodNameAsync, produzca una excepción InvalidOperationException.
En términos generales, no produzca una excepción si se invoca varias veces el método
MethodNameAsync sin el parámetro userSuppliedState, de manera que haya varias
operaciones pendientes. Puede provocar una excepción cuando la clase no puede
controlar explícitamente esa situación, pero se supone que los desarrolladores pueden
controlar todas estas devoluciones de llamada imposibles de distinguir.
Si está creando una clase que deriva de Component, no implemente ni instale su propia
clase SynchronizationContext. Los modelos de la aplicación, no los componentes, son
los que controlan la clase SynchronizationContext que se utiliza.
Al utilizar multithreading de cualquier tipo, el usuario se expone potencialmente a
errores muy serios y complejos.
2.2.1.4. Decidir cuándo implementar el modelo asincrónico basado en
eventos
El Modelo asincrónico basado en evento proporciona un modelo para exponer el
comportamiento asincrónico de una clase. Con la introducción de este modelo, .NET
Framework define dos modelos para exponer el comportamiento asincrónico: el Modelo
asincrónico basado en la interfaz System.IAsyncResult y el modelo basado en eventos. En este
tema se explica cuándo es adecuado implementar ambos modelos.
Principios generales
En general, siempre que sea posible debería exponer las características asincrónicas mediante el
Modelo asincrónico basado en evento. Sin embargo, hay algunos requisitos que no puede
cumplir el modelo basado en eventos. En esos casos, puede ser necesario implementar el
modelo IAsyncResult además del modelo basado en eventos.
Nota
Es raro que se implemente el modelo IAsyncResult sin que también se implemente el modelo
basado en eventos.
Instrucciones
La lista siguiente incluye instrucciones sobre cuándo se debería implementar el Modelo
asincrónico basado en evento:
Utilice el modelo basado en eventos como la API predeterminada para exponer el
comportamiento asincrónico de su clase.
No exponga el modelo IAsyncResult cuando su clase se utilice principalmente en una
aplicación cliente como, por ejemplo, Windows Forms.
Exponga el modelo IAsyncResult sólo cuando sea necesario para cumplir sus requisitos.
Por ejemplo, la compatibilidad con una API existente puede requerir que se exponga el
modelo IAsyncResult.
No exponga el modelo IAsyncResult sin exponer también el modelo basado en evento.
Si debe exponer el modelo IAsyncResult, hágalo como una opción avanzada. Por
ejemplo, si genera un objeto de servidor proxy, genere de forma predeterminada el
modelo basado en eventos, con una opción para generar el modelo IAsyncResult.
Compile su implementación del modelo basado en eventos en su implementación del
modelo IAsyncResult.
Evite exponer el modelo basado en eventos y el modelo IAsyncResult en la misma
clase. Exponga el modelo basado en eventos en las clases de "alto nivel" y el modelo
IAsyncResult en clases de "nivel inferior". Por ejemplo, compare el modelo basado en
eventos en el componente WebClient con el modelo IAsyncResult en la clase
HttpRequest.
o Exponga el modelo basado en eventos y el modelo IAsyncResult en la misma
clase cuando lo requiera la compatibilidad. Por ejemplo, si ya ha publicado una
API que utiliza el modelo IAsyncResult, sería necesario conservar el modelo
IAsyncResult para la compatibilidad con versiones anteriores.
o Exponga el modelo basado en eventos y el modelo IAsyncResult en la misma
clase si la complejidad del modelo de objetos resultante no compensará la
ventaja de separar las implementaciones. Es mejor exponer ambos modelos en
una sola clase que evitar exponer el modelo basado en evento.
para cada cálculo de número primo. Aunque comprobar si un número elevado es primo puede
llevar una cantidad de tiempo considerable, el subproceso de interfaz de usuario principal no se
verá interrumpido por este retraso, y el formulario permanecerá receptivo durante el cálculo.
Podrá ejecutar tantos cálculos como desee simultáneamente, así como cancelar de forma
selectiva los cálculos pendientes.
Las tareas ilustradas en este tutorial incluyen:
Crear el componente
Definir delegados y eventos asincrónicos públicos
Definir delegados privados
Implementar eventos públicos
Implementar el método de finalización
Implementar los métodos de trabajo
Implementar métodos de inicio y cancelación
Crear el componente
El primer paso es crear el componente que implementará el modelo asincrónico basado en
eventos.
Para crear el componente
Cree una clase denominada PrimeNumberCalculator que herede de Component.
Definir delegados y eventos asincrónicos públicos
Un componente se comunica con los clientes mediante eventos. El evento
MethodNameCompleted avisa a los clientes de la finalización de una tarea asincrónica y el
evento MethodNameProgressChanged informa a los clientes sobre el progreso de dicha tarea.
Para definir eventos asincrónicos para los clientes de un componente:
1. Importe los espacios de nombres System.Threading y System.Collections.Specialized
que están en la parte superior del archivo.
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Windows.Forms;
public CalculatePrimeCompletedEventArgs(
int numberToTest,
int firstDivisor,
bool isPrime,
Exception e,
bool canceled,
object state) : base(e, canceled, state)
{
this.numberToTestValue = numberToTest;
this.firstDivisorValue = firstDivisor;
this.isPrimeValue = isPrime;
}
Punto de control
En este punto, ya puede compilar el componente.
Punto de control
En este punto, ya puede compilar el componente.
Para probar el componente
Compile el componente.
Recibirá una advertencia del compilador:
warning CS0169: The private field
'AsynchronousPatternExample.PrimeNumberCalculator.workerDelegate' is
never used
}
}
//CalculatePrimeState calcState = new CalculatePrimeState(
// numberToTest,
// firstDivisor,
// isPrime,
// e,
// TaskCanceled(asyncOp.UserSuppliedState),
// asyncOp);
//this.CompletionMethod(calcState);
this.CompletionMethod(numberToTest,firstDivisor,isPrime,e,
TaskCanceled(asyncOp.UserSuppliedState),asyncOp);
//completionMethodDelegate(calcState);
}
Punto de control
En este punto, ya puede compilar el componente.
Para probar el componente
Compile el componente.
Lo único que falta por escribir son los métodos para iniciar y cancelar operaciones
asincrónicas, CalculatePrimeAsyncyCancelAsync.
Punto de control
En este punto, ya puede compilar el componente.
Para probar el componente
Compile el componente.
El componente PrimeNumberCalculator ya está completo y listo para su uso.
2.2.1.5.1. Cómo: Implementar un componente que admita el modelo
asincrónico basado en eventos
En el ejemplo de código siguiente se implementa un componente con un método asincrónico,
según Información general sobre el modelo asincrónico basado en eventos. El componente es
una calculadora de número primo que utiliza el algoritmo de Eratosthenes o de Sieve para
determinar si un número es primo o compuesto.
Visual Studio ofrece una amplia compatibilidad para esta tarea.
Para obtener un ejemplo de cliente que utilice el componente PrimeNumberCalculator, vea
Cómo: Implementar un cliente en un modelo asincrónico basado en eventos.
Ejemplo
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Windows.Forms;
...
/////////////////////////////////////////////////////////////
#region PrimeNumberCalculator Implementation
public delegate void ProgressChangedEventHandler(
ProgressChangedEventArgs e);
public delegate void CalculatePrimeCompletedEventHandler(
object sender, CalculatePrimeCompletedEventArgs e);
// This class implements the Event-based Asynchronous Pattern.
// It asynchronously computes whether a number is prime or
// composite (not prime).
public class PrimeNumberCalculator : Component
{
private delegate void WorkerEventHandler(int numberToCheck,
AsyncOperation asyncOp);
private SendOrPostCallback onProgressReportDelegate;
private SendOrPostCallback onCompletedDelegate;
private HybridDictionary userStateToLifetime = new HybridDictionary();
private System.ComponentModel.Container components = null;
/////////////////////////////////////////////////////////////
#region Public events
public event ProgressChangedEventHandler ProgressChanged;
public event CalculatePrimeCompletedEventHandler
CalculatePrimeCompleted;
#endregion
/////////////////////////////////////////////////////////////
#region Construction and destruction
public PrimeNumberCalculator()
{
InitializeComponent();
InitializeDelegates();
}
/////////////////////////////////////////////////////////////
#region Implementation
if (asyncOp != null)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(taskId);
}
}
}
e = new CalculatePrimeProgressChangedEventArgs(n,
(int)((float)n / (float)numberToTest * 100),
asyncOp.UserSuppliedState);
asyncOp.Post(this.onProgressReportDelegate, e);
primes.Add(n);
// Yield the rest of this time slice.
Thread.Sleep(0);
}
// Skip even numbers.
n += 2;
}
return primes;
}
protected void
OnCalculatePrimeCompleted(CalculatePrimeCompletedEventArgs e)
{
if (CalculatePrimeCompleted != null)
{
CalculatePrimeCompleted(this, e);
}
}
/////////////////////////////////////////////////////////////
#region Component Designer generated code
#endregion
}
public class
CalculatePrimeProgressChangedEventArgs:ProgressChangedEventArgs
{
private int latestPrimeNumberValue = 1;
public CalculatePrimeProgressChangedEventArgs(
int latestPrime,
int progressPercentage,
object userToken) : base( progressPercentage, userToken )
{
this.latestPrimeNumberValue = latestPrime;
}
namespace AsyncOperationManagerExample
{
// This form tests the PrimeNumberCalculator component.
public class PrimeNumberCalculatorMain : System.Windows.Forms.Form
{
#region Private fields
this.primeNumberCalculator1.ProgressChanged +=
new ProgressChangedEventHandler(
primeNumberCalculator1_ProgressChanged);
this.listView1.SelectedIndexChanged +=
new EventHandler(listView1_SelectedIndexChanged);
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#endregion // Construction and destruction
#region Implementation
this.primeNumberCalculator1.CalculatePrimeAsync(testNumber,taskId);
}
#endregion // Implementation
#endregion
this.progressColHeader,
this.currentColHeader,
this.taskIdColHeader,
this.resultColHeader,
this.firstDivisorColHeader});
this.listView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.listView1.FullRowSelect = true;
this.listView1.GridLines = true;
this.listView1.Location = new System.Drawing.Point(3, 16);
this.listView1.Name = "listView1";
this.listView1.Size = new System.Drawing.Size(602, 160);
this.listView1.TabIndex = 0;
this.listView1.View = System.Windows.Forms.View.Details;
// testNumberColHeader
this.testNumberColHeader.Text = "Test Number";
this.testNumberColHeader.Width = 80;
// progressColHeader
this.progressColHeader.Text = "Progress";
// currentColHeader
this.currentColHeader.Text = "Current";
// taskIdColHeader
this.taskIdColHeader.Text = "Task ID";
this.taskIdColHeader.Width = 200;
// resultColHeader
this.resultColHeader.Text = "Result";
this.resultColHeader.Width = 80;
// firstDivisorColHeader
this.firstDivisorColHeader.Text = "First Divisor";
this.firstDivisorColHeader.Width = 80;
// panel2
this.panel2.Location = new System.Drawing.Point(200, 128);
this.panel2.Name = "panel2";
this.panel2.TabIndex = 2;
// PrimeNumberCalculatorMain
this.ClientSize = new System.Drawing.Size(608, 254);
this.Controls.Add(this.taskGroupBox);
this.Name = "PrimeNumberCalculatorMain";
this.Text = "Prime Number Calculator";
this.taskGroupBox.ResumeLayout(false);
this.buttonPanel.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
[STAThread]
static void Main()
{
Application.Run(new PrimeNumberCalculatorMain());
}
}
#endregion
public PrimeNumberCalculator()
{
InitializeComponent();
InitializeDelegates();
}
#region Implementation
}
// Start the asynchronous operation.
WorkerEventHandler workerDelegat=new
WorkerEventHandler(CalculateWorker);
workerDelegate.BeginInvoke(numberToTest,asyncOp,null,null);
}
// This method computes the list of prime numbers used by the IsPrime
method.
CalculatePrimeCompletedEventArgs e =
operationState as CalculatePrimeCompletedEventArgs;
OnCalculatePrimeCompleted(e);
}
protected void
OnCalculatePrimeCompleted(CalculatePrimeCompletedEventArgs e)
{
if (CalculatePrimeCompleted != null)
{
CalculatePrimeCompleted(this, e);
}
}
#endregion
#endregion
public class
CalculatePrimeProgressChangedEventArgs:ProgressChangedEventArgs
{
private int latestPrimeNumberValue = 1;
public CalculatePrimeCompletedEventArgs(
int numberToTest,
int firstDivisor,
bool isPrime,
Exception e,
bool canceled,
object state) : base(e, canceled, state)
{
this.numberToTestValue = numberToTest;
this.firstDivisorValue = firstDivisor;
this.isPrimeValue = isPrime;
}
{
get
{
// Raise an exception if the operation failed or was canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property value.
return isPrimeValue;
}
}
}
#endregion
}
4. Ejecute la aplicación.
A medida que vaya descargándose la imagen, podrá mover libremente el formulario,
minimizarlo y maximizarlo.
2.3. Modelo de programación asincrónica (APM)
Una operación asincrónica que utiliza el modelo de diseño IAsyncResult se implementa como
dos métodos denominados BeginnombreDeOperación y EndnombreDeOperación que empiezan
y terminan respectivamente la operación asincrónica nombreDeOperación. Por ejemplo, la clase
FileStream proporciona los métodos BeginRead y EndRead para leer de forma asincrónica bytes
de un archivo. Estos métodos implementan la versión asincrónica del método Read.
Después de llamar a BeginnombreDeOperación, una aplicación puede seguir ejecutando
instrucciones en el subproceso que realiza la llamada mientras la operación asincrónica se lleva
a cabo en un subproceso diferente. Para cada llamada a BeginnombreDeOperación, la aplicación
también debería llamar a EndnombreDeOperación para obtener los resultados de la operación.
Comenzar una operación asincrónica
El método BeginnombreDeOperación inicia la operación asincrónica nombreDeOperación y
devuelve un objeto que implementa la interfaz IAsyncResult. Los objetos IAsyncResult
almacenan información sobre una operación asincrónica. En la siguiente tabla se muestra
información sobre una operación asincrónica.
Miembro Descripción
Objeto específico opcional de aplicación que contiene información
AsyncState
sobre la operación asincrónica.
WaitHandle que se puede utilizar para bloquear la ejecución de la
AsyncWaitHandle
aplicación hasta que la operación asincrónica finaliza.
Valor que indica si la operación asincrónica se completó en el
CompletedSynchronously subproceso utilizado para llamar a BeginnombreDeOperación en
lugar de completarse en un subproceso ThreadPool independiente.
IsCompleted Valor que indica si la operación asincrónica ha finalizado.
Un método BeginnombreDeOperación toma todos los parámetros declarados en la firma de la
versión sincrónica del método que se pasen por valor o por referencia. Los parámetros out no
forman parte de la firma del método BeginnombreDeOperación. La firma del método
BeginnombreDeOperación también incluye dos parámetros adicionales. El primero define un
delegado AsyncCallback que hace referencia a un método al que se llama cuando finaliza la
operación asincrónica. El llamador puede especificar null (Nothing en Visual Basic) si no desea
que se invoque un método cuando la operación finaliza. El segundo parámetro adicional es un
objeto definido por el usuario. Este objeto se puede utilizar para pasar información de estado
específica de la aplicación al método invocado cuando la operación asincrónica finaliza. Si un
método BeginnombreDeOperación toma parámetros adicionales específicos de operación, como
una matriz de bytes para almacenar bytes leídos de un archivo, el objeto de estado de aplicación
y AsyncCallback son los últimos parámetros de la firma del método BeginnombreDeOperación.
Begin nombreDeOperación devuelve inmediatamente el control al subproceso que realiza la
llamada. Si el método BeginnombreDeOperación produce excepciones, será antes de que se
inicie la operación asincrónica. Si el método BeginnombreDeOperación provoca excepciones,
no se invoca el método de devolución de llamada.
Finalizar una operación asincrónica
El método EndnombreDeOperación finaliza la operación asincrónica nombreDeOperación. El
tipo del valor devuelto del método EndnombreDeOperación coincide con el devuelto por su
homólogo sincrónico y es específico de la operación asincrónica. Por ejemplo, el método
EndRead devuelve el número de bytes leídos de FileStream y el método EndGetHostByName
devuelve un objeto IPHostEntry que contiene información acerca de un equipo host. El método
EndnombreDeOperación toma cualquier parámetro out o ref declarado en la firma de la versión
sincrónica del método. Además de los parámetros del método sincrónico, el método
EndnombreDeOperación también incluye un parámetro IAsyncResult. Los llamadores deben
pasar la instancia devuelta por la llamada correspondiente a BeginnombreDeOperación.
Si la operación asincrónica representada por el objeto IAsyncResult no se ha completado cuando
se llama a EndnombreDeOperación, EndnombreDeOperación bloquea el subproceso que realiza
la llamada hasta que se completa la operación asincrónica. Las excepciones generadas por la
operación asincrónica se producen desde el método EndnombreDeOperación. No se ha definido
el efecto de llamar varias veces al método EndnombreDeOperación con el mismo objeto
IAsyncResult. Tampoco se ha definido la llamada al método EndnombreDeOperación con un
objeto IAsyncResult que no fue devuelto por el método Begin relacionado.
Nota
Para cualquiera de los escenarios indefinidos, los implementadores deberían considerar la
posibilidad de producir InvalidOperationException.
Nota
Los implementadores de este modelo de diseño deben avisar al llamador de que la operación
sincrónica ha finalizado estableciendo IsCompleted en True, llamando al método de devolución
de llamada asincrónico (si se ha especificado) y señalizando el objeto AsyncWaitHandle.
Los desarrolladores de aplicaciones disponen de varias opciones de diseño para obtener acceso a
los resultados de la operación asincrónica. La opción correcta depende de si la aplicación tiene
instrucciones que se pueden ejecutar mientras la operación finaliza. Si una aplicación no puede
realizar ningún otro trabajo hasta que reciba los resultados de la operación asincrónica, debe
bloquearse hasta que los resultados estén disponibles. Para establecer el bloqueo hasta que
finalice una operación asincrónica, puede recurrir a uno de los métodos siguientes:
Llamar a EndnombreDeOperación desde el subproceso principal de la aplicación, lo que
supone bloquear la ejecución de la aplicación hasta que la operación se complete.
Utilice el objeto AsyncWaitHandle para bloquear la ejecución de la aplicación hasta que
una o más operaciones hayan finalizado.
En el caso de las aplicaciones que no necesariamente deben bloquearse mientras la operación
asincrónica finaliza, puede recurrir a uno de los métodos siguientes:
Sondear el estado de ejecución de la operación mediante la comprobación periódica de
la propiedad IsCompleted y la realización de una llamada a EndnombreDeOperación
cuando se complete la operación.
Utilice un delegado AsyncCallback para especificar que se invoque un método cuando
finalice la operación.
2.3.1. Llamar a métodos asincrónicos mediante IAsyncResult
Los tipos de las bibliotecas de clases de otros fabricantes y de .NET Framework pueden
proporcionar métodos que permitan que una aplicación determinada siga ejecutándose mientras
se llevan a cabo operaciones asincrónicas en subprocesos diferentes con respecto al subproceso
de la aplicación principal. En las siguientes secciones se describen y se proporcionan ejemplos
de código que muestran las diferentes maneras en las que es posible llamar a métodos
asincrónicos que utilicen el modelo de diseño IAsyncResult.
Bloquear la ejecución de una aplicación al finalizar una operación asincrónica.
Bloquear la ejecución de una aplicación mediante AsyncWaitHandle.
Sondear el estado de una operación asincrónica.
Utilizar un delegado AsyncCallback para finalizar una operación asincrónica.
2.3.1.1. Bloquear la ejecución de una aplicación mediante
AsyncWaitHandle
Las aplicaciones que no pueden seguir realizando otra tarea mientras esperan los resultados de
una operación asincrónica deben bloquearse hasta que la operación finalice. Utilice una de las
opciones siguientes para bloquear el subproceso principal de la aplicación en cuestión a la
espera de que una operación asincrónica finalice:
Utilice la propiedad AsyncWaitHandle de la interfaz IAsyncResult devuelta por el
método BeginnombreDeOperación de la operación asincrónica. Este tema muestra la
ejecución de dicho procedimiento.
Llame al método EndnombreDeOperación de la operación asincrónica.
Las aplicaciones que utilizan uno o varios objetos WaitHandle para establecer bloqueos hasta
que se completa una operación asincrónica suelen llamar al método BeginnombreDeOperación,
realizan todas las tareas que se puedan realizar sin tener los resultados de dicha operación y, a
continuación, se bloquean hasta que se completa la operación asincrónica. Una aplicación se
puede bloquear en una única operación llamando a uno de los métodos WaitOne mediante
AsyncWaitHandle. Para bloquear la ejecución de una aplicación mientras espera que finalice un
conjunto de operaciones asincrónicas, almacene los objetos AsyncWaitHandle asociados en una
matriz y llame a uno de los métodos WaitAll. Para bloquear la ejecución de una aplicación
mientras espera que finalice alguno de los conjuntos de operaciones asincrónicas, almacene los
objetos AsyncWaitHandle asociados en una matriz y llame a uno de los métodos WaitAny.
Ejemplo
El ejemplo de código siguiente muestra cómo utilizar los métodos asincrónicos en la clase DNS
para recuperar información sobre el Sistema de nombres de dominio para un equipo
especificado por el usuario. El ejemplo muestra cómo realizar el bloqueo utilizando el objeto
WaitHandle asociado a la operación asincrónica. Observe que se pasa el valor null (Nothing en
Visual Basic) para los parámetros BeginGetHostByName, requestCallback y stateObject, puesto
que no son necesarios cuando se utiliza este procedimiento.
/* The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computer.*/
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class WaitUntilOperationCompletes
{
public static void Main(string[] args)
{
// Make sure the caller supplied a host name.
if (args.Length == 0 || args[0].Length == 0)
{
// Print a message and exit.
especificado por el usuario. Hay que observar que se pasa null (Nothing en Visual Basic) para
los parámetros BeginGetHostByNamerequestCallback y stateObject ya que estos argumentos no
son necesarios a la hora de utilizar este enfoque.
/* The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computer.*/
using System;
using System.Net;
using System.Net.Sockets;
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class BlockUntilOperationCompletes
{
public static void Main(string[] args)
{
// Make sure the caller supplied a host name.
if (args.Length == 0 || args[0].Length == 0)
{
// Print a message and exit.
Console.WriteLine("You must specify the name of a host
computer.");
return;
}
// Start the asynchronous request for DNS information.
// This example does not use a delegate or user-supplied object
// so the last two arguments are null.
IAsyncResult result = Dns.BeginGetHostEntry(args[0], null, null);
Console.WriteLine("Processing your request for information...");
// Do any additional work that can be done here.
try
{
// EndGetHostByName blocks until the process completes.
IPHostEntry host = Dns.EndGetHostEntry(result);
string[] aliases = host.Aliases;
IPAddress[] addresses = host.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases");
for (int i = 0; i < aliases.Length; i++)
{
Console.WriteLine("{0}", aliases[i]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses");
for (int i = 0; i < addresses.Length; i++)
{
Console.WriteLine("{0}",addresses[i].ToString());
}
}
}
catch (SocketException e)
{
Console.WriteLine("An exception occurred while processing the
request:
{0}", e.Message);
}
}
}
}
Utilice una de las siguientes opciones para seguir ejecutando instrucciones mientras se está a la
espera de que una operación asincrónica finalice:
Utilice la propiedad IsCompleted de la interfaz IAsyncResult devuelta por el método
BeginnombreDeOperación de la operación asincrónica para determinar si la operación
se ha completado. Este enfoque se conoce como sondeo y se muestra en este tema.
Utilice un delegado AsyncCallback para procesar los resultados de la operación
asincrónica en un subproceso independiente.
Ejemplo
El siguiente ejemplo de código muestra cómo utilizar los métodos asincrónicos en la clase Dns
con el fin de recuperar información sobre el Sistema de nombres de dominio para un equipo
especificado por el usuario. Este ejemplo inicia la operación asincrónica y, a continuación,
imprime puntos (".") en la consola hasta que la operación se haya completado. Hay que observar
que se pasa null (Nothing en Visual Basic) para los parámetros
BeginGetHostByNameAsyncCallback y Object ya que estos argumentos no son necesarios a la
hora de utilizar este enfoque.
/* The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computer.
This example polls to detect the end of the asynchronous operation.*/
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class PollUntilOperationCompletes
{
static void UpdateUserInterface()
{
// Print a period to indicate that the application
// is still working on the request.
Console.Write(".");
}
{
Console.WriteLine("Aliases");
for (int i = 0; i < aliases.Length; i++)
{
Console.WriteLine("{0}", aliases[i]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses");
for (int i = 0; i < addresses.Length; i++)
{
Console.WriteLine("{0}",addresses[i].ToString());
}
}
}
catch (SocketException e)
{
Console.WriteLine("An exception occurred while processing the
request:
{0}", e.Message);
}
}
}
}
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class UseDelegateForAsyncCallback
{
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
// Create a state object that holds each requested host name,
// an associated IPHostEntry object or a SocketException.
public class HostRequest
{
// Stores the requested host name.
if (host.Length > 0)
{
// Increment the request counter in a thread safe manner.
Interlocked.Increment(ref requestCounter);
// Create and store the state object for this request.
HostRequest request = new HostRequest(host);
hostData.Add(request);
// Start the asynchronous request for DNS information.
Dns.BeginGetHostEntry(host, callBack, request);
}
} while (host.Length > 0);
// The user has entered all of the host names for lookup.
// Now wait until the threads complete.
while (requestCounter > 0)
{
UpdateUserInterface();
}
// Display the results.
foreach(HostRequest r in hostData)
{
if (r.ExceptionObject != null)
{
Console.WriteLine("Request for host {0} returned the
following
error: {1}.", r.HostName, r.ExceptionObject.Message);
}
else
{
// Get the results.
IPHostEntry h = r.HostEntry;
string[] aliases = h.Aliases;
IPAddress[] addresses = h.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases for {0}", r.HostName);
for (int j = 0; j < aliases.Length; j++)
{
Console.WriteLine("{0}", aliases[j]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses for {0}",
r.HostName);
for (int k = 0; k < addresses.Length; k++)
{
Console.WriteLine("{0}",addresses[k].ToString());
}
}
}
}
}
Nota
La característica IntelliSense en Visual Studio 2005 muestra los parámetros de BeginInvoke y
EndInvoke. Si no está utilizando Visual Studio u otra herramienta similar, o si está utilizando
C# con Visual Studio 2005, consulte Modelo de programación asincrónica (APM), donde
encontrará una descripción de los parámetros definidos para estos métodos.
En los ejemplos de código de este tema se muestran cuatro de las formas más comunes de
utilizar los métodos BeginInvoke y EndInvoke para realizar llamadas asincrónicas. Después de
llamar a BeginInvoke, puede hacer lo siguiente:
Realizar algunas operaciones y, a continuación, llamar al método EndInvoke para que
mantenga un bloqueo hasta que se complete la llamada.
Obtener un objeto WaitHandle mediante la propiedad IAsyncResult.AsyncWaitHandle,
utilizar su método WaitOne para bloquear la ejecución hasta que se señalice
WaitHandle y, a continuación, llamar al método EndInvoke.
Sondear el resultado IAsyncResult devuelto por BeginInvoke para determinar cuándo se
completa la llamada asincrónica y, a continuación, llamar al método EndInvoke.
Pasar un delegado de un método de devolución de llamada a BeginInvoke. El método se
ejecuta en un subproceso ThreadPool una vez finalizada la llamada asincrónica. El
método de devolución de llamada llama a EndInvoke.
Importante
Con independencia de la técnica que utilice, llame siempre a EndInvoke para completar la
llamada asincrónica.
Definir el método Test y el delegado asincrónico
En los ejemplos de código siguientes se muestran distintas maneras de llamar al mismo método
de ejecución prolongada, TestMethod, de forma asincrónica. El método TestMethod muestra un
mensaje en la consola para indicar que ha comenzado el procesamiento, espera unos segundos y,
a continuación, finaliza. TestMethod tiene un parámetro out para mostrar la manera en que esos
parámetros se agregan a las firmas de BeginInvoke y EndInvoke. Los parámetros ref se pueden
controlar de manera similar.
En el ejemplo de código siguiente se muestra la definición de TestMethod y el delegado
denominado AsyncMethodCaller que se puede utilizar para llamar a TestMethod de forma
asincrónica. Para compilar cualquiera de los ejemplos de código, debe incluir las definiciones
del método TestMethod y el delegado AsyncMethodCaller.
Ejemplo
using System;
using System.Threading;
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncDemo
{
// The method to be executed asynchronously.
public string TestMethod(int callDuration, out int threadId)
{
Console.WriteLine("Test method begins.");
Thread.Sleep(callDuration);
threadId = Thread.CurrentThread.ManagedThreadId;
return String.Format("My call time was {0}.",
callDuration.ToString());
}
}
// The delegate must have the same signature as the method
// it will call asynchronously.
public delegate string AsyncMethodCaller(int callDuration, out int
threadId);
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
public static void Main()
{
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null,
null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.",
Thread.CurrentThread.ManagedThreadId);
// Call EndInvoke to wait for the asynchronous call to complete,
// and to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
Console.WriteLine("The call executed on thread {0}, with return
value
\"{1}\".", threadId, returnValue);
}
}
}
/* This example produces output similar to the following
Main thread 1 does some work.
Test method begins.
The call executed on thread 3, with return value "My call time was 3000.". */
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
static void Main()
{
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null,
null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.",
Thread.CurrentThread.ManagedThreadId);
// Wait for the WaitHandle to become signaled.
result.AsyncWaitHandle.WaitOne();
// Perform additional processing here.
// Call EndInvoke to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
// Close the wait handle.
result.AsyncWaitHandle.Close();
Console.WriteLine("The call executed on thread {0}, with return
value
\"{1}\".", threadId, returnValue);
}
}
}
/* This example produces output similar to the following:
Main thread 1 does some work.
Test method begins.
The call executed on thread 3, with return value "My call time was 3000.". */
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
static void Main() {
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
static void Main()
{
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
namespace Examples.AdvancedProgramming.AsynchronousOperations
{
// Create a class that factors a number.
public class PrimeFactorFinder
{
public static bool Factorize(int number,ref int primefactor1,ref int
primefactor2)
{
primefactor1 = 1;
primefactor2 = number;
// Factorize using a low-tech approach.
for (int i=2;i<number;i++)
{
if (0 == (number % i))
{
primefactor1 = i;
primefactor2 = number / i;
break;
}
}
if (1 == primefactor1 )
return false;
else
return true;
}
}
// Define the method that receives a callback when the results are
available.
public void FactorizedResults(IAsyncResult result)
{
int factor1=0;
int factor2=0;
// Extract the delegate from the
// System.Runtime.Remoting.Messaging.AsyncResult.
AsyncFactorCaller factorDelegate =
(AsyncFactorCaller)((AsyncResult)result).AsyncDelegate;
int number = (int) result.AsyncState;
// Obtain the result.
bool answer = factorDelegate.EndInvoke(ref factor1, ref factor2,
result);
// Output the results.
Console.WriteLine("On CallBack: Factors of {0} : {1} {2} - {3}",
number, factor1, factor2, answer);
waiter.Set();
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. 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.
La biblioteca (TPL) paralelas task admite el paralelismo de datos a través de la clase de
System.Threading.Tasks.Parallel. Esta clase proporciona implementaciones paralelas método-
basadas de para y los bucles de 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.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL.
// Sequential version
foreach (var item in sourceCollection)
{
Process(item);
}
// Parallel equivalent
Parallel.ForEach(sourceCollection, item => Process(item));
Cuando un bucle paralelo se ejecuta, la TPL crea particiones del origen de datos para que el
bucle pueda funcionar 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.
Nota
También puede proporcionar un programador o creador de particiones personalizado.
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 incluyen
ParallelLoopState, ParallelOptions, Parallel LoopResult, CancellationToken, y
CancellationTokenSource de la funcionalidad.
PLINQ admite el paralelismo de datos con sintaxis declarativa o de consulta.
3.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.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL.
Ejemplo
namespace MultiplyMatrices
{
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
class Program
{
#region Sequential_Loop
static void MultiplyMatricesSequential(double[,] matA, double[,] matB,
double[,] result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
for (int i = 0; i < matARows; i++)
{
for (int j = 0; j < matBCols; j++)
{
for (int k = 0; k < matACols; k++)
{
result[i, j] += matA[i, k] * matB[k, j];
}
}
}
}
#endregion
#region Parallel_Loop
#endregion
#region Main
static void Main(string[] args)
{
// Set up matrices. Use small values to better view
// result matrix. Increase the counts to see greater
// speedup in the parallel loop vs. the sequential loop.
int colCount = 180;
int rowCount = 2000;
int colCount2 = 270;
double[,] m1 = InitializeMatrix(rowCount, colCount);
double[,] m2 = InitializeMatrix(colCount, colCount2);
double[,] result = new double[rowCount, colCount2];
// First do the sequential version.
#endregion
#region Helper_Methods
#endregion
}
}
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.
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<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.
3.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<T>.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la PLINQ.
Ejemplo
namespace ForEachDemo
{
using System;
using System.Drawing; // requires system.Drawing.dll
using System.IO;
using System.Threading;
using System.Threading.Tasks;
class SimpleForEach
{
static void Main()
{
// A simple source for demonstration purposes. Modify this path as
necessary.
string[] files = System.IO.Directory.GetFiles
(@"C:\Users\Public\Pictures\Sample Pictures", "*.jpg");
string newDir = @"C:\Users\Public\Pictures\Sample
Pictures\Modified";
System.IO.Directory.CreateDirectory(newDir);
// Method signature: Parallel.ForEach(IEnumerable<TSource> source,
// Action<TSource> body)
Parallel.ForEach(files, currentFile =>
{
// The more computational work you do here, the greater
// the speedup compared to a sequential foreach loop.
string filename = System.IO.Path.GetFileName(currentFile);
System.Drawing.Bitmap bitmap = 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.
// But be aware: Thread contention for the Console slows down
parallel
// loops!!!
Console.WriteLine("Processing {0} on thread {1}", filename,
Thread.CurrentThread.ManagedThreadId);
Un bucle ForEach funciona como un bucle For. Se crea una partición de la colección de origen
y el trabajo se programa en varios subprocesos en función del 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 usar ForEach con una colección no genérica, puede emplear el método de extensión
Cast<TResult> para convertir la colección en una colección genérica, como se muestra en el
ejemplo siguiente:
Parallel.ForEach(nonGenericCollection.Cast<object>(),currentElement =>{});
Puede usar también Parallel LINQ (PLINQ) con el fin de paralelizar el procesamiento de los
orígenes de datos IEnumerable<T>. PLINQ permite usar una sintaxis de consulta declarativa
para expresar el comportamiento del bucle.
3.1.1.3. Cómo: Detener o interrumpir un bucle Parallel.For
class Test
{
static void Main()
{
StopLoop();
BreakAtThreshold();
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
results.Push(d);
if (d > .2)
{
// Might be called more than once!
loopState.Break();
Console.WriteLine("Break called at iteration {0}. d = {1}
", i, d);
Thread.Sleep(1000);
}
});
Console.WriteLine("results contains {0} elements",
results.Count());
}
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
namespace ThreadLocalFor
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
class Test
{
static void Main()
{
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
// Use type parameter to make subtotal a long, not an int
Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
{
subtotal += nums[j];
return subtotal;
},
(x) => Interlocked.Add(ref total, x)
);
Console.WriteLine("The total is {0}", total);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
}
Los dos primeros 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<TResult>, donde TResult es el tipo de la variable que
almacenará el estado local del subproceso. Tenga en cuenta que, en este ejemplo, se usa una
versión genérica del método y el parámetro de tipo es long (Long en Visual Basic). El parámetro
de tipo indica al compilador el tipo de la variable temporal que se usará para almacenar el estado
local del subproceso. La expresión () => 0 (Function() 0 en Visual Basic) de este ejemplo
significa que la variable local de subproceso se inicializa en 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:
() => new MyClass()
El cuarto parámetro de tipo es donde define la lógica del bucle. IntelliSense muestra 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 se define el método al que se llamará una vez, cuando todas las
iteraciones de este subproceso se 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.
3.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<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<T> que el método
invocará cuando todos los bucles se hayan completado. El método proporciona el valor final de
la 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<T>, no hay valor devuelto.
namespace ThreadLocalForEach
{
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
class Test
{
static void Main()
{
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
// First type parameter is the type of the source elements
// Second type parameter is the type of the local data (subtotal)
Parallel.ForEach<int, long>(nums, // source collection
() => 0, // method to initialize the local variable
(j, loop, subtotal) => // method invoked by the loop on each
iteration
{
class Program
{
static void Main()
{
int[] nums = Enumerable.Range(0, 10000000).ToArray();
CancellationTokenSource cts = new CancellationTokenSource();
// Use ParallelOptions instance to store the CancellationToken
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;
Console.WriteLine("Press any key to start. Press 'c' to cancel.");
Console.ReadKey();
// Run a task so that we can cancel from another thread.
Task.Factory.StartNew(() =>
{
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
Console.WriteLine("press any key to exit");
});
try
{
Parallel.ForEach(nums, po, (num) =>
{
double d = Math.Sqrt(num);
Console.WriteLine("{0} on {1}", d,
Thread.CurrentThread.ManagedThreadId);
po.CancellationToken.ThrowIfCancellationRequested();
});
}
catch (OperationCanceledException e)
{
Console.WriteLine(e.Message);
}
Console.ReadKey();
}
}
}
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
class Program
{
static void Main()
{
// Source must be array or IList.
var source = Enumerable.Range(0, 100000).ToArray();
// Partition the entire source array.
var rangePartitioner = Partitioner.Create(0, source.Length);
double[] results = new double[source.Length];
// Loop over the partitions in parallel.
Parallel.ForEach(rangePartitioner, (range, loopState) =>
{
// Loop over each range element without a delegate invocation.
for (int i = range.Item1; i < range.Item2; i++)
{
results[i] = source[i] * Math.PI;
}
});
Console.WriteLine("Operation complete. Print results? y/n");
char input = Console.ReadKey().KeyChar;
El enfoque mostrado en este ejemplo es útil cuando el bucle realiza una cantidad de trabajo
mínima. Cuando el trabajo se vuelve más costoso en los cálculos, obtendrá probablemente un
rendimiento igual o mejor si usa un bucle For o ForEach con el particionador predeterminado.
3.1.1.9. Cómo: Recorrer en iteración directorios con la clase paralela
En muchos casos, la iteración de archivo es una operación que se puede paralelizar fácilmente.
El tema Cómo: Recorrer en iteración directorios con PLINQ muestra la manera más fácil de
realizar esta tarea en muchos escenarios. Sin embargo, pueden surgir complicaciones cuando el
código tiene que tratar con los muchos tipos de excepciones que pueden surgir al obtener acceso
al sistema de archivos. En el ejemplo siguiente se muestra un enfoque para el problema. Usa una
iteración basada en la pila para recorrer todos los archivos y carpetas en un directorio
especificado y habilita el código para detectar y controlar diversas excepciones. Por supuesto, la
forma de controlar las excepciones depende de usted.
Ejemplo
En el ejemplo siguiente la iteración en los directorios se realiza de forma secuencial, pero el
procesamiento de los archivos se realiza en paralelo. Este enfoque es probablemente el mejor
cuando hay una tasa alta de directorios y archivos. También es posible ejecutar la iteración de
directorio y obtener acceso a cada archivo secuencialmente. Probablemente no es eficaz
paralelizar ambos bucles a menos que esté dirigido específicamente a un equipo con un gran
número de procesadores. Sin embargo, como en todos los casos, se debe probar
exhaustivamente la aplicación para determinar el mejor enfoque.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Parallel_File
{
class Program
{
static void Main(string[] args)
{
TraverseTreeParallelForEach(@"C:\Program Files", (f) =>
{
// For this demo we don't do anything with the data
// except to read it.
byte[] data = File.ReadAllBytes(f);
// For user interest, although it slows down the operation.
Console.WriteLine(f);
});
// Keep the console window open.
Console.ReadKey();
}
},
(c) =>
{
Interlocked.Exchange(ref fileCount, fileCount +
c);
});
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
// Here we just output a message and go on.
Console.WriteLine(ex.Message);
return true;
}
// Handle other exceptions here if necessary...
return false;
});
}
// Push the subdirectories onto the stack for traversal.
// This could also be done before handing the files.
foreach (string str in subDirs) dirs.Push(str);
}
// For diagnostic purposes.
Console.WriteLine("Processed {0} files in {1} milleseconds",
fileCount,
sw.ElapsedMilliseconds);
}
}
}
En este ejemplo, la E/S de archivo se realiza de forma sincrónica. Al trabajar con archivos
grandes o conexiones de red lentas, puede ser preferible obtener acceso a los archivos de forma
asincrónica. Puede combinar las técnicas de E/S asincrónica con la iteración paralela.
Tenga en cuenta que si se produce una excepción en el subproceso principal, los subprocesos
que inicia el método ForEach podrían seguir ejecutándose. Para detener estos subprocesos,
puede establecer una variable booleana en los controladores de excepciones y comprobar su
valor en cada iteración del bucle paralelo. Si el valor indica que se ha iniciado una excepción,
use la variable ParallelLoopState para detener o interrumpir el bucle.
3.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 término
paralelismo de tareas hace referencia a la ejecución simultánea de una o varias tareas
independientes. Una tarea representa una operación asincrónica y, en ciertos aspectos, se
asemeja a la creación de un nuevo subproceso o elemento de trabajo ThreadPool, pero con un
nivel de abstracción mayor. Las tareas proporcionan dos ventajas fundamentales:
Un uso más eficaz y más escalable de los recursos del sistema.
En segundo plano, las tareas se ponen en la cola del elemento ThreadPool, que se ha
mejorado con algoritmos (como el algoritmo de ascenso de colina o "hill-climbing")
que determinan y ajustan el número de subprocesos con el que se maximiza el
rendimiento. Esto hace que las tareas resulten relativamente ligeras y que, por tanto,
pueda crearse un gran número de ellas para habilitar un paralelismo pormenorizado.
Como complemento y para proporcionar el equilibrio de carga, se usan los conocidos
algoritmos de robo de trabajo.
Un mayor control mediante programación del que se puede conseguir con un
subproceso o un elemento de trabajo.
Las tareas y el marco que se crea en torno a ellas proporcionan un amplio conjunto de
API que admiten el uso de esperas, cancelaciones, continuaciones, control robusto de
excepciones, estado detallado, programación personalizada, y más.
Por estos dos motivos, en .NET Framework, las tareas son las API preferidas para código
multiproceso, asincrónico, y paralelo de escritura.
Crear y ejecutar tareas implícitamente
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.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL. Si no
está familiarizado con las expresiones lambda en C# o Visual Basic, vea Expresiones lambda en
PLINQ y TPL.
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
Nota
El número de instancias de Task que Invoke crea en segundo plano no es necesariamente igual
al número de delegados que se proporcionan. La TPL puede emplear varias optimizaciones,
sobre todo con grandes números de delegados.
Para tener un mayor control de la ejecución de tareas o para devolver un valor de la tarea, debe
trabajar con objetos Task más explícitamente.
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<TResult>, que se hereda
de Task. El objeto de tarea administra los detalles de la infraestructura y proporciona métodos y
propiedades a los que se puede obtener acceso desde el subproceso que realiza la llamada a lo
largo de la duración de la tarea. Por ejemplo, se puede tener acceso a la propiedad Status de una
tarea en cualquier momento para determinar si ha empezado a ejecutarse, si se ha ejecutado
hasta su finalización, si se ha cancelado o si se ha producido una excepción. El estado se
representa mediante la enumeración TaskStatus.
Cuando se crea una tarea, se proporciona un delegado de usuario que encapsula el código que la
tarea va a ejecutar. El delegado se puede expresar como un delegado con nombre, un método
anónimo o una expresión lambda. Las expresiones lambda pueden contener una llamada a un
método con nombre, tal y como se muestra en el siguiente ejemplo.
// Create a task and supply a user delegate by using a lambda
expression.
var taskA = new Task(() => Console.WriteLine("Hello from
taskA."));
// Start the task.
taskA.Start();
// Output a message from the joining thread.
Console.WriteLine("Hello from the calling thread.");
// Message from taskA should follow.
/* Output:
* Hello from the calling thread.
* Hello from taskA.
*/
También puede utilizar los métodos de Run para crear e iniciar una tarea en una operación. Para
administrar la tarea, los métodos de Run utilizan el programador de tareas predeterminado,
independientemente del que se asocia el programador de tareas al subproceso actual. Los
métodos de Run son la manera preferida de crear e iniciar tareas cuando más control sobre la
creación y la programación de la tarea no es necesario.
También se puede usar el método StartNew para crear e iniciar una tarea en una sola operación.
Utilice este método cuando la creación y la programación no tienen que ser independientes y
necesita más opciones de creación de la tarea o el uso de un programador concreto, o si necesita
pasar el estado adicional en la tarea a través de la propiedad de AsyncState, como se muestra en
el ejemplo siguiente.
// Create and start the task in one operation.
var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from
taskA."));
// Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.");
Task y Task<TResult> cada exponen una propiedad estática de Factory 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<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 Result se obtiene antes de que el cálculo finaliza, la
propiedad se bloqueará el subproceso hasta que el valor esté disponible.
Task<double>[] taskArray = new Task<double>[]
{
Task<double>.Factory.StartNew(() => DoComputation1()),
// May be written more conveniently like this:
Task.Factory.StartNew(() => DoComputation2()),
Task.Factory.StartNew(() => DoComputation3())
};
double[] results = new double[taskArray.Length];
for (int i = 0; i < taskArray.Length; i++)
results[i] = taskArray[i].Result;
Cuando se usa una expresión lambda para crear un delegado, tiene acceso a todas las variables
que están visibles en ese momento en el código fuente. Sin embargo, en algunos casos,
especialmente en los bucles, una expresión lambda no captura la variable como se espera.
Captura solo el valor final, no el valor tal y como se transforma después de cada iteración.
Puede obtener acceso al valor en cada iteración si proporciona un objeto de estado a una tarea a
través de su constructor, como se muestra en el ejemplo siguiente:
class MyCustomData
{
Este estado se pasa como argumento al delegado de la tarea, y se puede tener acceso al objeto de
tarea mediante la propiedad de AsyncState. Además, el paso de los datos a través del constructor
podría proporcionar una pequeña ventaja de rendimiento en algunos escenarios.
Identificador de tarea
Cada tarea recibe un identificador entero que la identifica de forma única en un dominio de
aplicación y se puede tener acceso mediante la propiedad de 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 solicite; por consiguiente, una tarea puede tener un identificador diferente cada vez que el
programa se ejecute.
Opciones de creación de tareas
La mayoría de las API que crean tareas proporcionan sobrecargas que aceptan un parámetro
TaskCreationOptions. Especificar una de estas opciones, indica al programador de tareas cómo
programar la tarea en el grupo de subprocesos. En la tabla siguiente se muestran las diversas
opciones de creación de tareas.
Elemento Descripción
Es la opción predeterminada si no se especifica ninguna opción. El
None
programador usa su heurística predeterminada para programar la tarea.
Especifica que la tarea debe programarse de modo que las tareas creadas
PreferFairness 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.
Especifica que una tarea debe crearse como elemento secundario asociado de
AttachedToParent
la tarea actual, si existe.
outer.Wait();
Console.WriteLine("Outer task completed.");
/* Output:
Outer task beginning.
Outer task completed.
Detached task completed.
*/
});
parent.Wait();
Console.WriteLine("Parent task completed.");
/* Output:
Parent task beginning.
Attached task completed.
Parent task completed.
*/
Una tarea puede utilizar la opción de DenyChildAttach de evitar que otras tareas asociado a la
tarea primaria.
Para tareas que esperan de finalizar
El tipo de System.Threading.Tasks.Task y el tipo de System.Threading.Tasks.Task<TResult>
proporcionan varias sobrecargas de un método de Task.Wait y de Task<TResult>.Wait que
permiten esperar una tarea. Además, las sobrecargas del método estático de Task.WaitAll y
método de Task.WaitAny permiten esperar el alguna o toda una matriz de tareas finalicen.
Normalmente, una tarea se espera por una de estas razones:
El subproceso principal depende del resultado final que se calcula mediante una tarea.
Hay que controlar las excepciones que pueden producirse en la tarea.
En el siguiente ejemplo se muestra el modelo básico donde el control de excepciones no está
implicado.
Task[] tasks = new Task[3]
{
Task.Factory.StartNew(() => MethodA()),
Task.Factory.StartNew(() => MethodB()),
Task.Factory.StartNew(() => MethodC())
};
//Block until all tasks complete.
Task.WaitAll(tasks);
// Continue on this thread...
Las clases de Task y de Task<TResult> proporcionan varios métodos que pueden ayudarle a
crear varias tareas para implementar modelos comunes y mejorar utilizan las características de
lenguaje asincrónicas proporcionadas por C#, Visual Basic, y F#. Esta sección describe
WhenAll, WhenAny, Delay, y los métodos de FromResult<TResult>.
Task.WhenAll
El método de Task.WhenAll asincrónica espera Task múltiple o los objetos de Task<TResult>
al final. Proporciona las versiones sobrecargadas que permiten esperar conjuntos no uniforme de
tareas. Por ejemplo, puede esperar Task multithreading y Task<TResult>, objetos en
completarse en una llamada al método.
Task.WhenAny
El método de Task.WhenAny asincrónica espera uno de Task múltiple o de los objetos de
Task<TResult> al final. Como en el método de Task.WhenAll, este método proporciona
versiones sobrecargadas que permiten esperar conjuntos no uniforme de tareas. El método de
WhenAny es especialmente útil en los escenarios siguientes.
Operaciones redundantes. Considere un algoritmo o una operación que se pueden
realizar en gran medida. Puede utilizar el método de WhenAny para seleccionar la
operación que finaliza primero y después cancelar operaciones restantes.
Operaciones intercaladas. Puede iniciar varias operaciones que deben finalizar y utilizar
el método de WhenAny para procesar resultados como cada operación finaliza. Después
de una operación finalice, puede iniciar una o más tareas adicionales.
Restringir las operaciones. Puede utilizar el método de WhenAny para extender el
escenario anterior limitando el número de operaciones simultáneas.
Operaciones expirado. Puede utilizar el método de WhenAny para seleccionar entre una
o más tareas y una tarea que termina después de un tiempo concreto, como una tarea
devuelta por el método de Delay. El método de Delay se describe en la sección
siguiente.
Task.Delay
El método de Task.Delay genera un objeto de Task que termina después de que el tiempo
especificado. Puede utilizar este método para crear bucles que sondean en ocasiones para los
datos, especifique tiempos, retrasan administrar de datos proporcionados por el usuario durante
un tiempo predeterminado, etc.
Tarea (T).FromResult
Utilizando el método de Task.FromResult<TResult>, puede crear un objeto de Task<TResult>
que celebre un resultado pre- calculado. Este método es útil al realizar una operación
asincrónica que devuelve un objeto de Task<TResult>, y el resultado de ese objeto de
Task<TResult> se calcula ya.
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 de unión con la tarea,
que normalmente es el subproceso que está esperando la tarea finalice o tenga acceso a la
propiedad de Result. 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 de Task admite la cancelación cooperativa y está totalmente integrada con la clase de
System.Threading.CancellationTokenSource y la clase de
System.Threading.CancellationToken, que son nuevas en .NET Framework 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 y de Run 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.
Cuando cree tareas de continuación a partir de, utilice el método del método o de
ContinueWhenAny de ContinueWhenAll o sus equivalentes en la clase de
Task<TResult>.
Para encapsular los métodos BeginX y EndX del modelo de programación asincrónica
en una instancia de Task o Task<TResult>, use los métodos FromAsync.
TaskFactory predeterminado se puede tener acceso como propiedad estática de la clase de Task
o la clase de Task<TResult>. También pueden crearse directamente instancias de TaskFactory y
especificar varias opciones entre las que se incluyan las opciones CancellationToken,
TaskCreationOptions, TaskContinuationOptions o TaskScheduler. Las opciones se especifican
al crear el generador de tareas se aplicará a todas las tareas que cree, a menos que Task se crea
mediante la enumeración de TaskCreationOptions en ese caso, las opciones de la tarea
reemplazan los del generador de tareas.
Tareas sin delegados
En algunos casos, es posible que desee usar un objeto Task para encapsular alguna operación
asincrónica ejecutada por un componente externo en lugar de su propio usuario delegado. Si la
operación se basa en el patrón Begin/End del modelo de programación asincrónica, puede usar
los métodos FromAsync. Si no es este el caso, puede usar el objeto
TaskCompletionSource<TResult> para encapsular la operación en una tarea y, de este modo,
aprovechar algunas de las ventajas de programación de Task, como por ejemplo, su
compatibilidad con la propagación de excepciones y el uso de continuaciones.
Programadores personalizados
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.
Task<int>[] tasks = new Task<int>[2];
tasks[0] = new Task<int>(() =>
{
// Do some work...
return 34;
});
tasks[1] = new Task<int>(() =>
{
// Do some work...
return 8;
});
var continuation =
Task.Factory.ContinueWhenAll(tasks,(antecedents) =>
{
int answer = tasks[0].Result +
tasks[1].Result;
Console.WriteLine("The answer is {0}",
answer);
});
tasks[0].Start();
tasks[1].Start();
continuation.Wait();
cts.Token
);
Task task2 = task.ContinueWith((antecedent) =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token);
task.Start();
// Antecedent and/or continuation will
// respond to this request, depending on when it is made.
cts.Cancel();
using System.Threading.Tasks;
// Demonstrates how to associate state with task continuations.
class ContinuationState
{
// Simluates a lengthy operation and returns the time at which
// the operation completed.
public static DateTime DoWork()
{
// Simulate work by suspending the current thread
// for two seconds.
Thread.Sleep(2000);
// Return the current time.
return DateTime.Now;
}
static void Main(string[] args)
{
// Start a root task that performs work.
Task<DateTime> t = Task<DateTime>.Run(delegate { return DoWork(); });
// Create a chain of continuation tasks, where each task is
// followed by another task that performs work.
List<Task<DateTime>> continuations = new List<Task<DateTime>>();
for (int i = 0; i < 5; i++)
{
// Provide the current time as the state of the continuation.
t = t.ContinueWith(delegate { return DoWork(); }, DateTime.Now);
continuations.Add(t);
}
// Wait for the last task in the chain to complete.
t.Wait();
// Print the creation time of each continuation (the state object)
// and the completion time (the result of that task) to the console.
foreach (var continuation in continuations)
{
DateTime start = (DateTime)continuation.AsyncState;
DateTime end = continuation.Result;
Console.WriteLine("Task was created at {0} and finished at {1}.",
start.TimeOfDay, end.TimeOfDay);
}
}
}
/* Sample output:
Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062.
Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646.
Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230.
Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883.
Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083.
*/
c.Wait();
}
catch (AggregateException ae)
{
foreach(var e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
Console.WriteLine("Exception handled. Let's move on.");
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 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.
Tareas Tareas secundarias
Categoría
anidadas asociadas
La tarea externa (primaria) espera a que las tareas internas
No Sí
se completen.
La tarea primaria propaga las excepciones iniciadas por las
No Sí
tareas secundarias (tareas internas).
El estado de la tarea primaria (tarea externa) depende del
No Sí
estado de la tarea secundaria (tarea interna).
En escenarios desasociados en los que la tarea anidada es un objetoTask<TResult>, se puede
forzar que la tarea primaria espere a la secundaria mediante el acceso a la propiedad Result de la
tarea anidada. La propiedad Result se bloquea hasta que su tarea se completa.
static void WaitForSimpleNestedTask()
{
var outer = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Outer task executing.");
var nested = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Nested task starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Nested task completing.");
return 42;
});
// Parent will wait for this detached child.
return nested.Result;
});
Console.WriteLine("Outer has returned {0}.", outer.Result);
}
/* Sample output:
Outer task executing.
Nested task starting.
Nested task completing.
Outer has returned 42.
*/
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.
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.
3.1.2.4. Control de excepciones
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. Las excepciones se propagan cuando
se usa uno de los métodos estáticos o de instancia Task.Wait o Task<TResult>.Wait, y estos
métodos se controlan si la llamada se enmarca en una instrucción 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 de InnerExceptions que se puede
enumerar para examinar todas las excepciones originales que se generaron, y administrar (o no)
cada individualmente. Aunque solo se inicie una única excepción, se encapsulará en un objeto
Aggregate Exception.
var task1 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("I'm bad, but not too bad!");
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
// Assume we know what's going on with this particular exception.
// Rethrow anything else. AggregateException.Handle provides
// another way to express this. See later example.
foreach (var e in ae.InnerExceptions)
{
if (e is MyCustomException)
{
Console.WriteLine(e.Message);
}
else
{
throw;
}
}
Para evitar una 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.
Nota
Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se
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.
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task1 = Task.Factory.StartNew(() =>
{
CancellationToken ct = token;
while (someCondition)
{
// Do some work...
Thread.SpinWait(50000);
ct.ThrowIfCancellationRequested();
}
},
token);
// No waiting required.
En el siguiente fragmento de código se muestra el uso del método Handle con la misma función.
ae.Handle((ex) =>
{
return ex is MyCustomException;
});
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
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.UnobservedTaskException EventArgs 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.
3.1.2.5. Cómo: Usar Parallel.Invoke para ejecutar operaciones
paralelas
Este ejemplo muestra cómo paralelizar las operaciones utilizando Invoke en la biblioteca TPL.
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.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL.
Ejemplo
namespace ParallelTasks
{
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
class ParallelInvoke
{
static void Main()
{
// Retrieve Darwin's "Origin of the Species" from Gutenberg.org.
string[] words =
CreateWordArray(@"http://www.gutenberg.org/files/2009/2009.txt");
#region ParallelTasks
// Perform three tasks in parallel on the source array
Parallel.Invoke(() =>
{
Console.WriteLine("Begin first task...");
GetLongestWord(words);
}, // close first Action
() =>
{
Console.WriteLine("Begin second task...");
GetMostCommonWords(words);
}, //close second Action
() =>
{
Console.WriteLine("Begin third task...");
GetCountForWord(words, "species");
} //close third Action
); //close parallel.invoke
Console.WriteLine("Returned from Parallel.Invoke");
#endregion
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
#region HelperMethods
private static void GetCountForWord(string[] words, string term)
{
var findWord = from word in words
where word.ToUpper().Contains(term.ToUpper())
select word;
Console.WriteLine(@"Task 3 -- The word ""{0}"" occurs {1} times.",
term, findWord.Count());
}
Console.WriteLine(sb.ToString());
}
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.
3.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<TResult> para
devolver un valor de la propiedad Result.
Ejemplo
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Return a value type with a lambda expression
Task<int> task1 = Task<int>.Factory.StartNew(() => 1);
int i = task1.Result;
// Return a named reference type with a multi-line statement lambda.
Task<Test> task2 = Task<Test>.Factory.StartNew(() =>
{
string s = ".NET";
double d = 4.0;
return new Test { Name = s, Number = d };
});
Test test = task2.Result;
// Return an array produced by a PLINQ query
Task<string[]> task3 = Task<string[]>.Factory.StartNew(() =>
{
string path = @"C:\users\public\pictures\";
string[] files = System.IO.Directory.GetFiles(path);
var result = (from file in files.AsParallel()
let info = new System.IO.FileInfo(file)
where info.Extension == ".jpg"
select file).ToArray();
return result;
});
foreach (var name in task3.Result)
Console.WriteLine(name);
}
class Test
{
public string Name { get; set; }
public double Number { get; set; }
}
}
La propiedad Result bloquea el subproceso que realiza la llamada hasta que la tarea finaliza.
3.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<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
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static Random rand = new Random();
static void Main(string[] args)
{
// Wait on a single task with no timeout specified.
Task taskA = Task.Factory.StartNew(() => DoSomeWork(10000000));
taskA.Wait();
Console.WriteLine("taskA has completed.");
// Wait on a single task with a timeout specified.
Task taskB = Task.Factory.StartNew(() => DoSomeWork(10000000));
taskB.Wait(100); //Wait for 100 ms.
if (taskB.IsCompleted)
Console.WriteLine("taskB has completed.");
else
Console.WriteLine("Timed out before taskB completed.");
// Wait for all tasks to complete.
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
tasks[i] = Task.Factory.StartNew(() => DoSomeWork(10000000));
}
Task.WaitAll(tasks);
// Wait for first task to complete.
Task<double>[] tasks2 = new Task<double>[3];
// Try three different approaches to the problem. Take the first
one.
tasks2[0] = Task<double>.Factory.StartNew(() => TrySolution1());
tasks2[1] = Task<double>.Factory.StartNew(() => TrySolution2());
tasks2[2] = Task<double>.Factory.StartNew(() => TrySolution3());
int index = Task.WaitAny(tasks2);
double d = tasks2[index].Result;
Console.WriteLine("task[{0}] completed first with result of {1}.",
index, d);
Console.ReadKey();
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press any key to start. Press 'c' to cancel.");
Console.ReadKey();
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
// Store references to the tasks so that we can wait on them and
// observe their status after cancellation.
Task[] tasks = new Task[10];
// Request cancellation of a single task when the token source is
canceled.
// Pass the token to the user delegate, and also to the task so it
can
// handle the exception correctly.
tasks[0] = Task.Factory.StartNew(() => DoSomeWork(1, token),
token);
// Request cancellation of a task and its children. Note the token
is passed
// to (1) the user delegate and (2) as the second argument to
StartNew, so
// that the task instance can correctly handle the
OperationCanceledException.
tasks[1] = Task.Factory.StartNew(() =>
{
// Create some cancelable child tasks.
for (int i = 2; i < 10; i++)
{
// For each child task, pass the same token
// to each user delegate and to StartNew.
tasks[i] = Task.Factory.StartNew(iteration =>
DoSomeWork((int)iteration, token), i, token);
}
// Passing the same token again to do work on the parent task.
// All will be signaled by the call to tokenSource.Cancel
below.
DoSomeWork(2, token);
}, token);
// Give the tasks a second to start.
Thread.Sleep(1000);
// Request cancellation from the UI thread.
if (Console.ReadKey().KeyChar == 'c')
{
tokenSource.Cancel();
Console.WriteLine("\nTask cancellation requested.");
// Optional: Observe the change in the Status property on the
task.
// It is not necessary to wait on tasks that have canceled.
However,
// if you do wait, you must enclose the call in a try-catch
block to
// catch the OperationCanceledExceptions that are thrown. If
you do
// not wait, no OCE is thrown if the token that was passed to
the
// StartNew method is the same token that requested the
cancellation.
#region Optional_WaitOnTasksToComplete
try
{
Task.WaitAll(tasks);
}
catch (AggregateException e)
{
// For demonstration purposes, show the OCE message.
foreach (var v in e.InnerExceptions)
Console.WriteLine("msg: " + v.Message);
}
// Prove that the tasks are now all in a canceled state.
for (int i = 0; i < tasks.Length; i++)
Console.WriteLine("task[{0}] status is now {1}", i,
tasks[i].Status);
#endregion
}
// Keep the console window open while the
// task completes its output.
Console.ReadLine();
}
}
}
}
}
}
namespace ContinueWith
{
class Continuations
{
static void Main()
{
SimpleContinuation();
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
{
d += Math.Pow(mean - n, 2);
}
return Math.Sqrt(d / (values.Length - 1));
}
}
}
El parámetro de tipo de Task<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.
3.1.2.11. Cómo: Crear tareas precalculadas
Este documento se describe cómo usar el método de Task.FromResult<TResult> para recuperar
los resultados de las operaciones asincrónicas de descarga que se retienen en la memoria caché.
El método de FromResult<TResult> devuelve un objeto terminado de Task<TResult> que
celebre el valor proporcionado como su propiedad de Result . Este método es útil al realizar una
operación asincrónica que devuelve un objeto de Task<TResult> , y el resultado de ese objeto
de Task<TResult> se calcula ya.
Ejemplo
El ejemplo siguiente descarga las cadenas de web. define el método de DownloadStringAsync .
Este método descarga las cadenas de web de forma asincrónica. Este ejemplo también utiliza un
objeto de ConcurrentDictionary<TKey, TValue> almacenar en caché los resultados de
operaciones anteriores. Si se celebra la dirección de la entrada en esta memoria caché,
DownloadStringAsync utiliza el método de FromResult<TResult> para generar un objeto de
Task<TResult> que contiene el contenido en esa dirección. Si no, DownloadStringAsync
descarga el archivo web y agrega el resultado a la memoria caché.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
Este ejemplo calcula el tiempo necesario para descargar varias cadenas dos veces. El segundo
conjunto de operaciones de descarga debe tardar menos tiempo que el primer conjunto porque
los resultados se retienen en la memoria caché. El método de FromResult<TResult> permite que
el método de DownloadStringAsync para crear los objetos de Task<TResult> que contienen
estos resultados pre-calculados.
3.1.2.12. 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.
Ejemplo
public class TreeWalk
{
static void Main()
{
Tree<MyClass> tree = new Tree<MyClass>();
// ...populate tree (left as an exercise)
// Define the Action to perform on each node.
Action<MyClass> myAction = x => Console.WriteLine("{0} : {1}",
x.Name,
x.Number);
// Traverse the tree with parallel tasks.
DoTree(tree, myAction);
}
// By using Parallel.Invoke
public static void DoTree2<T>(Tree<T> tree, Action<T> action)
{
if (tree == null) return;
Parallel.Invoke(
() => DoTree2(tree.Left, action),
() => DoTree2(tree.Right, action),
() => action(tree.Data)
);
}
}
Los dos 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.
3.1.2.13. 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:
static Task<string> DoWorkAsync()
{
return Task<String>.Factory.StartNew(() =>
{
//...
return "Work completed.";
});
}
Task<String> t = DoWorkAsync();
t.Wait();
Console.WriteLine(t.Result);
}
Aunque es posible escribir 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 se deben controlar las
excepciones y también las solicitudes de 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.
Task<string> t3 = DoWorkAsync().ContinueWith((s) =>
DoMoreWorkAsync()).Unwrap();
// Outputs "More work completed."
Console.WriteLine(t.Result);
#region Dummy_Methods
private static byte[] GetData()
{
Random rand = new Random();
byte[] bytes = new byte[64];
rand.NextBytes(bytes);
return bytes;
}
byte final = 0;
foreach (byte item in data)
{
final ^= item;
Console.WriteLine("{0:x}", final);
}
Console.WriteLine("Done computing");
return final;
}
#endregion
}
}
Dado que una tarea primaria no finaliza hasta que todo el final secundario de tareas, una tarea
secundaria de ejecución prolongada puede provocar la aplicación total para ejecutarse mal. En
este ejemplo, cuando la aplicación utiliza las opciones predeterminadas de crear la tarea
primaria, la tarea secundaria debe finalizar antes de que la tarea primaria finaliza. Cuando la
aplicación utiliza la opción de TaskCreationOptions.DenyChildAttach , no está asociado al
elemento secundario al elemento primario. Por consiguiente, la aplicación puede realizar el
trabajo adicional después de que la tarea primaria finaliza y antes de que debe esperar la tarea
secundaria finalice.
3.1.3. Biblioteca de procesamiento paralelo basado en tareas (TPL)
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 4. El propósito de la biblioteca TPL es aumentar la
productividad de los desarrolladores al simplificar el proceso de agregar paralelismo y
simultaneidad a las aplicaciones. La biblioteca TPL escala el grado de simultaneidad de forma
dinámica para usar más eficazmente todos los procesadores que están disponibles. 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.
También puede utilizar el método de TryReceive para leer un bloque de flujo de datos, como se
muestra en el ejemplo siguiente. El método de TryReceive no bloquea el subproceso actual y es
útil cuando se sondea en ocasiones para los datos.
// Post more messages to the block.
for (int i = 0; i < 3; i++)
{
bufferBlock.Post(i);
}
// Receive the messages back from the block.
int value;
while (bufferBlock.TryReceive(out value))
{
Console.WriteLine(value);
}
/* Output:
0
1
2
*/
Un ejemplo completo
El ejemplo siguiente se muestra el código completo de este documento.
using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
{
// Create an array to hold random byte data.
byte[] buffer = new byte[1024];
// Fill the buffer with random bytes.
rand.NextBytes(buffer);
// Post the result to the message block.
target.Post(buffer);
}
// Set the target to the completed state to signal to the consumer
// that no more data will be available.
target.Complete();
}
TOutput> para leer el archivo y para calcular el número de bytes cero, y ActionBlock<TInput>
para imprimir el número de bytes cero en la consola. El objeto de TransformBlock<TInput,
TOutput> especifica un objeto de Func<T, TResult> para realizar el trabajo cuando los bloques
reciben datos. El objeto de ActionBlock<TInput> usa una expresión lambda para imprimir en la
consola el número de bytes cero se lean que.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
También puede utilizar expresiones asincrónicas lambda para realizar una acción en un bloque
de flujo de datos de la ejecución. El ejemplo siguiente se modifica el objeto de
TransformBlock<TInput, TOutput> que se utiliza en el ejemplo anterior para que use una
expresión lambda para realizar el trabajo de forma asincrónica.
// Create a TransformBlock<string, int> object that calls the
// CountBytes function and returns its result.
var countBytesAsync = new TransformBlock<string, int>(async path =>
{
byte[] buffer = new byte[1024];
int totalZeroBytesRead = 0;
using (var fileStream = new FileStream(
path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, true))
{
int bytesRead = 0;
do
{
// Asynchronously read from the file stream.
bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length);
totalZeroBytesRead += buffer.Count(b => b == 0);
} while (bytesRead > 0);
}
return totalZeroBytesRead;
});
{
Console.WriteLine("Filtering word list...");
return words.Where(word => word.Length > 3).OrderBy(word => word)
.Distinct().ToArray();
});
// Finds all words in the specified collection whose reverse also
// exists in the collection.
var findPalindromes = new TransformManyBlock<string[], string>(words =>
{
Console.WriteLine("Finding palindromes...");
// Holds palindromes.
var palindromes = new ConcurrentQueue<string>();
// Add each word in the original collection to the result whose
// palindrome also exists in the collection.
Parallel.ForEach(words, word =>
{
// Reverse the work.
string reverse = new string(word.Reverse().ToArray());
// Enqueue the word if the reversed version also exists
// in the collection.
if (Array.BinarySearch<string>(words, reverse) >= 0 &&
word != reverse)
{
palindromes.Enqueue(word);
}
});
return palindromes;
});
// Prints the provided palindrome to the console.
var printPalindrome = new ActionBlock<string>(palindrome =>
{
Console.WriteLine("Found palindrome {0}/{1}",
palindrome, new string(palindrome.Reverse().ToArray()));
});
Miembro Tipo Descripción
TransformBlock<TInput,
downloadString Descarga el texto del libro de web.
TOutput>
TransformBlock<TInput, Separa el texto del libro en una matriz de
createWordList
TOutput> palabras.
Quita palabras cortas de matriz de word,
TransformBlock<TInput,
filterWordList ordena las palabras resultantes
TOutput>
alfabéticamente, y quita los duplicados.
Busca todas las palabras en la colección
TransformManyBlock<TInput, filtrada de matriz de la palabra cuyo
findPalindromes
TOutput> reverse también aparece en la matriz de
word.
printPalindrome ActionBlock<TInput> Imprime palíndromos en la consola.
Aunque puede combinar varios pasos en la canalización de flujo de datos en este ejemplo en un
paso, el ejemplo muestra el concepto de crear tareas independientes varias de flujo de datos para
realizar una tarea mayor. El ejemplo utiliza TransformBlock<TInput, TOutput> para permitir
que cada miembro de la canalización para realizar una operación en los datos de entrada y
enviar los resultados al paso siguiente en la canalización. El miembro de findPalindromes de la
canalización es un objeto de TransformManyBlock<TInput, TOutput> porque genera los
resultados independientes varios para cada entrada. La cola de la canalización,
printPalindrome, es un objeto de ActionBlock<TInput> porque realiza una acción en la
entrada, y no genera un resultado.
Formación de canalización
Agregue el código siguiente para conectar cada bloque a bloque siguiente en la canalización.
Cuando se llama al método de LinkTo para conectar un bloque de flujo de datos de origen a un
bloque de flujo de datos de destino, los datos de las propagaciones de bloques de flujo de datos
de origen al destino bloqueado como datos está disponible.
//
// Connect the dataflow blocks to form a pipeline.
//
downloadString.LinkTo(createWordList);
createWordList.LinkTo(filterWordList);
filterWordList.LinkTo(findPalindromes);
findPalindromes.LinkTo(printPalindrome);
Este ejemplo envía una dirección URL a través de la canalización de flujo de datos que se
procese. Si envía más de uno escrito a través de una canalización, llame al método de
IDataflowBlock.Complete después de enviar todas las entradas. Puede omitir este paso si la
aplicación no tiene ningún punto bien definido en el que los datos ya no están disponibles o la
aplicación no tiene que esperar la canalización finalice.
Esperar la canalización para finalizar
Agregue el código siguiente para esperar la canalización finalice. Dado que este ejemplo utiliza
tareas de continuación para propagar la finalización a través de la canalización, finaliza la
operación global cuando la cola de los finals de canalización.
// Wait for the last block in the pipeline to process all messages.
printPalindrome.Completion.Wait();
if (t.IsFaulted)
((IDataflowBlock)printPalindrome).Fault(t.Exception);
else printPalindrome.Complete();
});
// Process "The Iliad of Homer" by Homer.
downloadString.Post("http://www.gutenberg.org/files/6130/6130-0.txt");
// Mark the head of the pipeline as complete. The continuation tasks
// propagate completion through the pipeline as each part of the
// pipeline finishes.
downloadString.Complete();
// Wait for the last block in the pipeline to process all messages.
printPalindrome.Completion.Wait();
}
}
/* Sample output:
Downloading 'http://www.gutenberg.org/files/6130/6130-0.txt'...
Creating word list...
Filtering word list...
Finding palindromes...
Found palindrome doom/mood
Found palindrome draw/ward
Found palindrome live/evil
Found palindrome seat/taes
Found palindrome aera/area
Found palindrome mood/doom
Found palindrome moor/room
Found palindrome sleek/keels
Found palindrome area/aera
Found palindrome evil/live
Found palindrome speed/deeps
Found palindrome spot/tops
Found palindrome spots/stops
Found palindrome stops/spots
Found palindrome taes/seat
Found palindrome port/trop
Found palindrome tops/spot
Found palindrome trop/port
Found palindrome reed/deer
Found palindrome deeps/speed
Found palindrome deer/reed
Found palindrome ward/draw
Found palindrome room/moor
Found palindrome keels/sleek
*/
{
// Setting MaxMessages to one instructs
// the source block to unlink from the WriteOnceBlock<T> object
// after offering the WriteOnceBlock<T> object one message.
source.LinkTo(writeOnceBlock, new DataflowLinkOptions { MaxMessages =
1 });
}
// Return the first value that is offered to the WriteOnceBlock object.
return writeOnceBlock.Receive();
}
Para recibir el valor de primer TransformBlock<TInput, TOutput> opóngase que los finals, este
ejemplo definen el método de ReceiveFromAny(T) . El método de ReceiveFromAny(T)
acepta una matriz de los objetos de ISourceBlock<TOutput> y vínculos cada uno de estos
objetos a un objeto de WriteOnceBlock<T> . Cuando se utiliza el método del LinkTo para
vincular un bloque de flujo de datos de origen a un bloque de destino, el origen propaga
mensajes al destino mientras los datos disponible. Dado que la clase de WriteOnceBlock<T>
solo acepta el primer mensaje que proporciona, el método de ReceiveFromAny(T) genera el
resultado llamando al método de Receive . Esto muestra el primer mensaje que se proporciona
al objeto de WriteOnceBlock<T>. El método de LinkTo tiene una versión sobrecargada que
toma un parámetro de Boolean , unlinkAfterOne que, cuando se establece en True, pida al
origen para bloquear desenlazar de destino después del destino recibe un mensaje de origen. Es
importante que el objeto de WriteOnceBlock<T> desvincular de sus orígenes porque la relación
entre la matriz de orígenes y el objeto de WriteOnceBlock<T> ya no se requiere después de que
el objeto de WriteOnceBlock<T> recibe un mensaje.
Para que las llamadas restantes a TrySolution para finalizar después de que una de ellas calcula
un valor, el método de TrySolution toma un objeto de CancellationToken que se cancele
después de la llamada a ReceiveFromAny(T) vuelva. El método de SpinUntil devuelve cuando
este objeto de CancellationToken se cancela.
3.1.3.6. Tutorial: Usar flujos de datos en aplicaciones de Windows
Forms
Este documento se muestra cómo crear una red de bloques de flujo de datos que realizan el
procesamiento de imágenes en una aplicación de Windows Forms.
Este ejemplo carga los archivos de imagen de la carpeta especificada, crea una imagen
compuesta, y muestra el resultado. El ejemplo utiliza el modelo de flujo de datos para enrutar
imágenes a través de la red. En el modelo de flujo de datos, los componentes independientes de
un programa se comunican entre sí enviando mensajes. Cuando un componente recibe un
mensaje, realiza alguna acción y pasa el resultado a otro componente. Compare esto con el
modelo de flujo de control, en el que una aplicación usa estructuras de control, como
instrucciones condicionales, bucles, etc., para controlar el orden de las operaciones en un
programa.
Crear la aplicación de formularios Windows Forms
En esta sección se describe cómo crear una aplicación básica de Windows Forms y agregar
controles al formulario principal.
Para crear la aplicación de Windows Forms
1. En Visual Studio, cree Visual C# o un proyecto de Visual Basic Aplicación de
Windows Forms . En este documento, el proyecto se denomina CompositeImages.
2. En el diseñador de formularios para el formulario principal, Form1.cs (Form1.vb para
Visual Basic), agregue un control de ToolStrip .
3. Agregue un control de ToolStripButton al control de ToolStrip . Establezca la propiedad
de DisplayStyle a Text y la propiedad de Text para elegir la carpeta.
4. Agregue un control de ToolStripButton de segundo al control de ToolStrip . Establezca
la propiedad de DisplayStyle a Text, la propiedad de Text para cancelar, y la propiedad
de Enabled a False.
5. Agregue un objeto de PictureBox al formulario principal. Establezca la propiedad Dock
en Fill.
Crear la red de flujo de datos
En esta sección se describe cómo crear la red de flujo de datos que realiza el procesamiento de
imágenes.
Para crear la red de flujo de datos
1. Agregue una referencia a System.Threading.Tasks.Dataflow.dll al proyecto.
2. Asegúrese de que Form1.cs (Form1.vb para Visual Basic) contiene los siguientes
extractos de using (Using en Visual Basic):
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
ImageLockMode.ReadOnly,PixelFormat.Format32bppArgb))
.ToList();
// Compute each column in parallel.
Parallel.For(0, largest.Width, new ParallelOptions
{
CancellationToken = cancellationTokenSource.Token
},
i =>
{
// Compute each row.
for (int j = 0; j < largest.Height; j++)
{
// Counts the number of bitmaps whose dimensions
// contain the current location.
int count = 0;
// The sum of all alpha, red, green, and blue components.
int a = 0, r = 0, g = 0, b = 0;
// For each bitmap, compute the sum of all color components.
foreach (var bitmapData in bitmapDataList)
{
Environment.GetFolderPath(Environment.SpecialFolder.CommonPictures),
"Sample Pictures");
if (Directory.Exists(initialDirectory))
{
dlg.SelectedPath = initialDirectory;
}
// Show the dialog and process the dataflow network.
if (dlg.ShowDialog() == DialogResult.OK)
{
// Create a new CancellationTokenSource object to enable
cancellation.
cancellationTokenSource = new CancellationTokenSource();
// Create the image processing network if needed.
if (headBlock == null)
{
headBlock = CreateImageProcessingNetwork();
}
// Post the selected path to the network.
headBlock.Post(dlg.SelectedPath);
// Enable the Cancel button and disable the Choose Folder button.
toolStripButton1.Enabled = false;
toolStripButton2.Enabled = true;
// Show a wait cursor.
Cursor = Cursors.WaitCursor;
}
}
Ejemplo completo
El ejemplo siguiente se muestra el código completo de este tutorial.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
namespace CompositeImages
{
public Form1()
{
InitializeComponent();
}
});
// Create a dataflow block that responds to a cancellation request by
// displaying an image to indicate that the operation is cancelled
and
// enables the user to select another folder.
var operationCancelled = new ActionBlock<object>(delegate
{
// Display the error image to indicate that the operation
// was cancelled.
pictureBox1.SizeMode = PictureBoxSizeMode.CenterImage;
pictureBox1.Image = pictureBox1.ErrorImage;
// Enable the user to select another folder.
toolStripButton1.Enabled = true;
toolStripButton2.Enabled = false;
Cursor = DefaultCursor;
},
// Specify a task scheduler from the current synchronization
context
// so that the action runs on the UI thread.
new ExecutionDataflowBlockOptions
{
TaskScheduler =
TaskScheduler.FromCurrentSynchronizationContext()
});
// Connect the network.
// Link loadBitmaps to createCompositeBitmap.
// The provided predicate ensures that createCompositeBitmap accepts
the
// collection of bitmaps only if that collection has at least one
member.
loadBitmaps.LinkTo(createCompositeBitmap, bitmaps => bitmaps.Count()
> 0);
// Also link loadBitmaps to operationCancelled.
// When createCompositeBitmap rejects the message, loadBitmaps
// offers the message to operationCancelled.
// operationCancelled accepts all messages because we do not provide
a
// predicate.
loadBitmaps.LinkTo(operationCancelled);
// Link createCompositeBitmap to displayCompositeBitmap.
// The provided predicate ensures that displayCompositeBitmap accepts
the
// bitmap only if it is non-null.
createCompositeBitmap.LinkTo(displayCompositeBitmap, bitmap => bitmap
!=
null);
// Also link createCompositeBitmap to operationCancelled.
// When displayCompositeBitmap rejects the message,
createCompositeBitmap
// offers the message to operationCancelled.
// operationCancelled accepts all messages because we do not provide
a
// predicate.
createCompositeBitmap.LinkTo(operationCancelled);
// Return the head of the network.
return loadBitmaps;
}
// Loads all bitmap files that exist at the provided path.
IEnumerable<Bitmap> LoadBitmaps(string path)
{
List<Bitmap> bitmaps = new List<Bitmap>();
// Load a variety of image types.
foreach (string bitmapType in
new string[] { "*.bmp", "*.gif", "*.jpg", "*.png", "*.tif" })
{
// Load each bitmap for the current extension.
foreach (string fileName in Directory.GetFiles(path, bitmapType))
{
{
unsafe
{
byte*row=(byte*)(bitmapData.Scan0 + (j *
bitmapData.Stride));
byte* pix = (byte*)(row + (4 * i));
a += *pix; pix++;
r += *pix; pix++;
g += *pix; pix++;
b += *pix;
}
count++;
}
}
unsafe
{
// Compute the average of each color component.
a /= count;
r /= count;
g /= count;
b /= count;
// Set the result pixel.
byte*row=(byte*)(resultBitmapData.Scan0 +
(j*resultBitmapData.Stride));
byte* pix = (byte*)(row + (4 * i));
*pix = (byte)a; pix++;
*pix = (byte)r; pix++;
*pix = (byte)g; pix++;
*pix = (byte)b;
}
}
});
// Unlock the source bitmaps.
for (int i = 0; i < bitmapArray.Length; i++)
{
bitmapArray[i].UnlockBits(bitmapDataList[i]);
}
// Unlock the result bitmap.
result.UnlockBits(resultBitmapData);
// Return the result.
return result;
}
// Event handler for the Choose Folder button.
private void toolStripButton1_Click(object sender, EventArgs e)
{
// Create a FolderBrowserDialog object to enable the user to
// select a folder.
FolderBrowserDialog dlg = new FolderBrowserDialog
{
ShowNewFolderButton = false
};
// Set the selected path to the common Sample Pictures folder
// if it exists.
string initialDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonPictures),
"Sample Pictures");
if (Directory.Exists(initialDirectory))
{
dlg.SelectedPath = initialDirectory;
}
// Show the dialog and process the dataflow network.
if (dlg.ShowDialog() == DialogResult.OK)
{
// Create a new CancellationTokenSource object to enable
cancellation.
cancellationTokenSource = new CancellationTokenSource();
// Create the image processing network if needed.
if (headBlock == null)
{
headBlock = CreateImageProcessingNetwork();
}
// Post the selected path to the network.
headBlock.Post(dlg.SelectedPath);
// Enable the Cancel button and disable the Choose Folder button.
toolStripButton1.Enabled = false;
toolStripButton2.Enabled = true;
// Show a wait cursor.
Cursor = Cursors.WaitCursor;
}
}
// Event handler for the Cancel button.
private void toolStripButton2_Click(object sender, EventArgs e)
{
// Signal the request for cancellation. The current component of
// the dataflow network will respond to the cancellation request.
cancellationTokenSource.Cancel();
}
}
}
La ilustración siguiente muestra el resultado típico para las imágenes \ carpeta de común \ de
ejemplo.
{
CancellationToken = cancellationSource.Token
});
// Create the second, and final, node in the pipeline.
completeWork = new ActionBlock<WorkItem>(workItem =>
{
// Perform some work.
workItem.DoWork(1000);
// Decrement the progress bar that tracks the count of
// active work items in this stage of the pipeline.
decrementProgress.Post(toolStripProgressBar2);
// Increment the progress bar that tracks the overall
// count of completed work items.
incrementProgress.Post(toolStripProgressBar3);
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
MaxDegreeOfParallelism = 2
});
// Connect the two nodes of the pipeline.
startWork.LinkTo(completeWork);
// When the first node completes, set the second node also to
// the completed state.
startWork.Completion.ContinueWith(delegate { completeWork.Complete();
});
// Create the dataflow action blocks that increment and decrement
// progress bars.
// These blocks use the task scheduler that is associated with
// the UI thread.
incrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value++,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
decrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value--,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
}
Esta sección describe cómo conectar la canalización de flujo de datos a la interfaz de usuario.
Creando la canalización y agregando elementos de trabajo a la canalización son controlados por
el controlador de eventos para el botón de los elementos de trabajo add. La cancelación es
iniciada por el botón Cancelar. Cuando el usuario hace clic en cualquiera de ellos, la acción
adecuada se inicia de forma asincrónica.
Ejemplo
El ejemplo siguiente se muestra el código completo de Form1.cs (Form1.vb para Visual Basic).
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
namespace CancellationWinForms
{
public partial class Form1 : Form
{
// A placeholder type that performs work.
class WorkItem
{
// Performs work for the provided number of milliseconds.
public void DoWork(int milliseconds)
{
// For demonstration, suspend the current thread.
Thread.Sleep(milliseconds);
}
}
// Enables the user interface to signal cancellation.
CancellationTokenSource cancellationSource;
// The first node in the dataflow pipeline.
TransformBlock<WorkItem, WorkItem> startWork;
// The second, and final, node in the dataflow pipeline.
ActionBlock<WorkItem> completeWork;
// Increments the value of the provided progress bar.
ActionBlock<ToolStripProgressBar> incrementProgress;
// Decrements the value of the provided progress bar.
ActionBlock<ToolStripProgressBar> decrementProgress;
// Enables progress bar actions to run on the UI thread.
TaskScheduler uiTaskScheduler;
public Form1()
{
InitializeComponent();
// Create the UI task scheduler from the current sychronization
// context.
uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}
});
// Create the second, and final, node in the pipeline.
completeWork = new ActionBlock<WorkItem>(workItem =>
{
// Perform some work.
workItem.DoWork(1000);
// Decrement the progress bar that tracks the count of
// active work items in this stage of the pipeline.
decrementProgress.Post(toolStripProgressBar2);
// Increment the progress bar that tracks the overall
// count of completed work items.
incrementProgress.Post(toolStripProgressBar3);
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
MaxDegreeOfParallelism = 2
});
// Connect the two nodes of the pipeline.
startWork.LinkTo(completeWork);
// When the first node completes, set the second node also to
// the completed state.
startWork.Completion.ContinueWith(delegate { completeWork.Complete();
});
// Create the dataflow action blocks that increment and decrement
// progress bars.
// These blocks use the task scheduler that is associated with
// the UI thread.
incrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value++,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
decrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value--,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
}
toolStripButton1.Enabled = false;
toolStripButton2.Enabled = false;
// Trigger cancellation.
cancellationSource.Cancel();
try
{
// Asynchronously wait for the pipeline to complete processing and
for
// the progress bars to update.
await
Task.WhenAll(completeWork.Completion,incrementProgress.Completion,
decrementProgress.Completion);
}
catch (OperationCanceledException)
{
}
// Increment the progress bar that tracks the number of cancelled
// work items by the number of active work items.
toolStripProgressBar4.Value += toolStripProgressBar1.Value;
toolStripProgressBar4.Value += toolStripProgressBar2.Value;
// Reset the progress bars that track the number of active work
items.
toolStripProgressBar1.Value = 0;
toolStripProgressBar2.Value = 0;
// Enable the Add Work Items button.
toolStripButton1.Enabled = true;
}
}
}
Esta técnica es útil si necesita funcionalidad personalizada de flujo de datos, pero no requiere un
tipo que proporciona métodos adicionales, propiedades, campos o.
// Creates a IPropagatorBlock<T, T[]> object propagates data in a
// sliding window fashion.
public static IPropagatorBlock<T, T[]> CreateSlidingWindow<T>(int windowSize)
{
// Create a queue to hold messages.
var queue = new Queue<T>();
// The target part receives data and adds them to the queue.
var target = new ActionBlock<T>(item =>
{
// Add the item to the queue.
queue.Enqueue(item);
// Remove the oldest item when the queue size exceeds the window size.
if (queue.Count > windowSize)
queue.Dequeue();
// Post the data in the queue to the source block when the queue size
// equals the window size.
if (queue.Count == windowSize)
source.Post(queue.ToArray());
});
// When the target is set to the completed state, propagate out any
// remaining data and set the source to the completed state.
target.Completion.ContinueWith(delegate
{
if (queue.Count > 0 && queue.Count < windowSize)
source.Post(queue.ToArray());
source.Complete();
});
// Attempts to remove all available elements from the source into a new
// array that is returned.
public bool TryReceiveAll(out IList<T[]> items)
{
return m_source.TryReceiveAll(out items);
}
#endregion
#endregion
#endregion
// Signals to this target block that it should not accept any more
messages,
// nor consume postponed messages.
public void Complete()
{
m_target.Complete();
}
#endregion
}
Ejemplo completo
El ejemplo siguiente se muestra el código completo de este tutorial. También muestra cómo
utilizar el ambos que deslizan bloques de ventana en un método que escriba el bloque, lea de
ella, e imprimir los resultados en la consola.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
// Attempts to remove all available elements from the source into a new
// array that is returned.
public bool TryReceiveAll(out IList<T[]> items)
{
return m_source.TryReceiveAll(out items);
}
#endregion
#endregion
#endregion
// Signals to this target block that it should not accept any more
messages,
// nor consume postponed messages.
public void Complete()
{
m_target.Complete();
}
#endregion
}
{
Console.Write(comma);
Console.Write(item);
comma = ",";
}
Console.Write("}");
windowComma = ", ";
});
// Link the printer block to the sliding window block.
slidingWindow.LinkTo(printWindow);
// Set the printer block to the completed state when the sliding window
// block completes.
slidingWindow.Completion.ContinueWith(delegate { printWindow.Complete();
});
// Print an additional newline to the console when the printer block
completes.
var completion = printWindow.Completion.ContinueWith(delegate {
Console.WriteLine(); });
// Post the provided values to the sliding window block and then wait
// for the sliding window block to complete.
foreach (T value in values)
{
slidingWindow.Post(value);
}
slidingWindow.Complete();
// Wait for the printer to complete and perform its final action.
completion.Wait();
}
MaxDegreeOfParallelism = maxDegreeOfParallelism
});
// Compute the time that it takes for several messages to
// flow through the dataflow block.
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < messageCount; i++)
{
workerBlock.Post(1000);
}
workerBlock.Complete();
// Wait for all messages to propagate through the network.
workerBlock.Completion.Wait();
// Stop the timer and return the elapsed number of milliseconds.
stopwatch.Stop();
return stopwatch.Elapsed;
}
static void Main(string[] args)
{
int processorCount = Environment.ProcessorCount;
int messageCount = processorCount;
// Print the number of processors on this computer.
Console.WriteLine("Processor count = {0}.", processorCount);
TimeSpan elapsed;
// Perform two dataflow computations and print the elapsed
// time required for each.
// This call specifies a maximum degree of parallelism of 1.
// This causes the dataflow block to process messages serially.
elapsed = TimeDataflowComputations(1, messageCount);
Console.WriteLine("Degree of parallelism = {0}; message count = {1}; " +
"elapsed time = {2}ms.", 1, messageCount,
(int)elapsed.TotalMilliseconds);
// Perform the computations again. This time, specify the number of
// processors as the maximum degree of parallelism. This causes
// multiple messages to be processed in parallel.
elapsed = TimeDataflowComputations(processorCount, messageCount);
Console.WriteLine("Degree of parallelism = {0}; message count = {1}; " +
"elapsed time = {2}ms.", processorCount, messageCount,
(int)elapsed.TotalMilliseconds);
}
}
/* Sample output:
Processor count = 4.
Degree of parallelism = 1; message count = 4; elapsed time = 4032ms.
Degree of parallelism = 4; message count = 4; elapsed time = 1001ms.
*/
namespace WriterReadersWinForms
{
public partial class Form1 : Form
{
// Broadcasts values to an ActionBlock<int> object that is associated
// with each check box.
BroadcastBlock<int> broadcaster = new BroadcastBlock<int>(null);
public Form1()
{
InitializeComponent();
// Create an ActionBlock<CheckBox> object that toggles the state
// of CheckBox objects.
// Specifying the current synchronization context enables the
// action to run on the user-interface thread.
var toggleCheckBox = new ActionBlock<CheckBox>(checkBox =>
{
checkBox.Checked = !checkBox.Checked;
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
});
// Create a ConcurrentExclusiveSchedulerPair object.
// Readers will run on the concurrent part of the scheduler pair.
// The writer will run on the exclusive part of the scheduler pair.
var taskSchedulerPair = new ConcurrentExclusiveSchedulerPair();
// Create an ActionBlock<int> object for each reader CheckBox object.
// Each ActionBlock<int> object represents an action that can read
// from a resource in parallel to other readers.
// Specifying the concurrent part of the scheduler pair enables the
// reader to run in parallel to other actions that are managed by
// that scheduler.
var readerActions =
from checkBox in new CheckBox[] {checkBox1, checkBox2, checkBox3}
select new ActionBlock<int>(milliseconds =>
{
// Toggle the check box to the checked state.
toggleCheckBox.Post(checkBox);
// Perform the read action. For demonstration, suspend the
current
// thread to simulate a lengthy read operation.
Thread.Sleep(milliseconds);
// Toggle the check box to the unchecked state.
toggleCheckBox.Post(checkBox);
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = taskSchedulerPair.ConcurrentScheduler
});
// Create an ActionBlock<int> object for the writer CheckBox object.
// This ActionBlock<int> object represents an action that writes to
// a resource, but cannot run in parallel to readers.
// Specifying the exclusive part of the scheduler pair enables the
// writer to run in exclusively with respect to other actions that
are
// managed by the scheduler pair.
var writerAction = new ActionBlock<int>(milliseconds =>
{
// Toggle the check box to the checked state.
toggleCheckBox.Post(checkBox4);
// Perform the write action. For demonstration, suspend the
current
// thread to simulate a lengthy write operation.
Thread.Sleep(milliseconds);
// Toggle the check box to the unchecked state.
toggleCheckBox.Post(checkBox4);
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = taskSchedulerPair.ExclusiveScheduler
});
return result;
}
// Retrieves the ID of the first employee that has the provided name.
static int GetEmployeeID(string lastName, string firstName, string
connectionString)
{
using (SqlCeConnection connection = new SqlCeConnection(connectionString))
{
SqlCeCommand command = new SqlCeCommand(
string.Format("SELECT [Employee ID] FROM Employees " +
"WHERE [Last Name] = '{0}' AND [First Name] = '{1}'",lastName,
firstName),
connection);
connection.Open();
try
{
return (int)command.ExecuteScalar();
}
finally
{
connection.Close();
}
}
}
El método de AddEmployees agrega datos aleatorios employee en la base de datos con el flujo
de datos. Crea un objeto de ActionBlock<TInput> que llame al método de InsertEmployees
para agregar una entrada de empleados en la base de datos. El método de AddEmployees llama
al método de PostRandomEmployees para enviar varios objetos de Employee al objeto de
ActionBlock<TInput> . El método de AddEmployees esperar todas las operaciones de
inserción finalice.
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks.Dataflow;
{
// Create the SQL command.
SqlCeCommand command = new SqlCeCommand(
"INSERT INTO Employees ([Last Name], [First Name])" +
"VALUES (@lastName, @firstName)",connection);
connection.Open();
for (int i = 0; i < employees.Length; i++)
{
// Set parameters.
command.Parameters.Clear();
command.Parameters.Add("@lastName", employees[i].LastName);
command.Parameters.Add("@firstName",
employees[i].FirstName);
// Execute the command.
command.ExecuteNonQuery();
}
}
finally
{
connection.Close();
}
}
}
// Retrieves the ID of the first employee that has the provided name.
static int GetEmployeeID(string lastName, string firstName,
string connectionString)
{
using (SqlCeConnection connection = new
SqlCeConnection(connectionString))
{
SqlCeCommand command = new SqlCeCommand(
string.Format("SELECT [Employee ID] FROM Employees " +
"WHERE [Last Name] = '{0}' AND [First Name] = '{1}'",
lastName, firstName), connection);
connection.Open();
try
{
return (int)command.ExecuteScalar();
}
finally
{
connection.Close();
}
}
batchEmployees.Completion.ContinueWith(delegate{insertEmployees.Complete();
});
// Post several random Employee objects to the batch block.
PostRandomEmployees(batchEmployees, count);
// Set the batch block to the completed state and wait for
// all insert operations to complete.
batchEmployees.Complete();
insertEmployees.Completion.Wait();
}
// to the console.
var printEmployees =
new ActionBlock<Tuple<IList<Employee>, IList<Exception>>>(data =>
{
// Print information about the employees in this batch.
Console.WriteLine("Received a batch...");
foreach (Employee e in data.Item1)
{
Console.WriteLine("Last={0} First={1} ID={2}",
e.FirstName, e.LastName, e.EmployeeID);
}
// Print the error count for this batch.
Console.WriteLine("There were {0} errors in this batch...",
data.Item2.Count);
// Update total error count.
totalErrors += data.Item2.Count;
});
// Link the batched join block to the action block.
selectEmployees.LinkTo(printEmployees);
// When the batched join block completes, set the action block also to
complete
selectEmployees.Completion.ContinueWith(delegate{printEmployees.Complete();});
// Try to retrieve the ID for several random employees.
Console.WriteLine("Selecting random entries from Employees
table...");
for (int i = 0; i < count; i++)
{
try
{
// Create a random employee.
Employee e = Employee.Random();
// Try to retrieve the ID for the employee from the database.
e.EmployeeID = GetEmployeeID(e.LastName, e.FirstName,
connectionString);
// Post the Employee object to the Employee target of
// the batched join block.
selectEmployees.Target1.Post(e);
}
catch (NullReferenceException e)
{
// GetEmployeeID throws NullReferenceException when there is
// no such employee with the given name. When this happens,
// post the Exception object to the Exception target of
// the batched join block.
selectEmployees.Target2.Post(e);
}
}
// Set the batched join block to the completed state and wait for
// all retrieval operations to complete.
selectEmployees.Complete();
printEmployees.Completion.Wait();
// Print the total error count.
Console.WriteLine("Finished. There were {0} total errors.",
totalErrors);
}
La biblioteca TPL se puede utilizar con los modelos asincrónicos tradicionales de programación
de .NET Framework de maneras diferentes.
3.1.4.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<TResult>.
Ajustar las operaciones de APM en una tarea
Las clases System.Threading.Tasks.TaskFactory y System.Threading.Tasks.TaskFactory
<TResult> proporcionan varias sobrecargas de los métodosFromAsync yFromAsyncque
permiten encapsular un par de métodos Begin/End en una instancia de Task o de
Task<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<TResult>, que crean un objeto Task<TResult>. Para los métodos
End 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 Task<TResult> FromAsync<TArg1, TArg2, TArg3>(
Func<TArg1, TArg2, TArg3, AsyncCallback, object, IAsyncResult>
beginMethod, //BeginRead
Func<IAsyncResult, TResult> endMethod, //EndRead
TArg1 arg1, // the byte[] buffer
TArg2 arg2, // the offset in arg1 at which to start writing data
TArg3 arg3, // the maximum number of bytes to read
El primer parámetro es un delegado Func<T1, T2, T3, T4, T5, TResult> que coincide con la
signatura del método FileStream.BeginRead. El segundo parámetro es un delegado Func<T,
TResult> que toma una interfaz IAsyncResult y devuelve TResult. Dado que EndRead devuelve
un entero, el compilador deduce el tipo de TResult como Int32 y el tipo de la tarea como Task.
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.
Desplazamiento en el búfer donde deben comenzar a escribirse 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, 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 que encapsula el par
BeginRead/ EndRead de la clase FileStream.
const int MAX_FILE_SIZE = 14000000;
public static Task<string> GetFileStringAsync(string path)
{
FileInfo fi = new FileInfo(path);
byte[] data = null;
data = new byte[fi.Length];
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read,
FileShare.Read, data.Length, true);
//Task<int> returns the number of bytes read
Task<int> task = Task<int>.Factory.FromAsync(
fs.BeginRead, fs.EndRead, data, 0, data.Length, null);
// It is possible to do other work here while waiting
// for the antecedent task to complete.
// Add the continuation, which returns a Task<string>.
return task.ContinueWith((antecedent) =>
{
fs.Close();
// Result = "number of bytes read" (if we need it.)
if (antecedent.Result < 100)
{
return "Data is too small to bother with.";
}
else
{
// If we did not receive the entire file, the end of the
// data buffer will contain garbage.
if (antecedent.Result < data.Length)
Array.Resize(ref data, antecedent.Result);
// Will be returned in the Result property of the Task<string>
// at some future point after the asynchronous file I/O operation
completes.
return new UTF8Encoding().GetString(data);
}
});
}
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.TaskCompletion Source<TResult>, que se puede usar para representar
cualquier conjunto arbitrario de operaciones como Task<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<TResult> para exponer
un conjunto de operaciones WebClient asincrónicas al código de cliente como un objeto
Task<TResult> 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.
Task<string[]> GetWordCountsSimplified(string[] urls, string name,
CancellationToken token)
{
TaskCompletionSource<string[]> tcs = new TaskCompletionSource<string[]>();
WebClient[] webClients = new WebClient[urls.Length];
object m_lock = new object();
int count = 0;
List<string> results = new List<string>();
// If the user cancels the CancellationToken, then we can use the
// WebClient's ability to cancel its own async operations.
token.Register(() =>
{
foreach (var wc in webClients)
{
if (wc != null)
wc.CancelAsync();
}
});
for (int i = 0; i < urls.Length; i++)
{
webClients[i] = new WebClient();
#region callback
// Specify the callback for the DownloadStringCompleted
// event that will be raised by this WebClient instance.
webClients[i].DownloadStringCompleted += (obj, args) =>
{
// Argument validation and exception handling omitted for brevity.
// Split the string into an array of words, then count the number
// of elements that match the search term.
string[] words = args.Result.Split(' ');
string NAME = name.ToUpper();
int nameCount = (from word in words.AsParallel()
where word.ToUpper().Contains(NAME)
select word)
.Count();
// Associate the results with the url, and add new string to the
array
// that the underlying Task object will return in its Result
property.
return;
}
else if (args.Error != null)
{
// Pass through to the underlying Task
// any exceptions thrown by the WebClient
// during the asynchronous operation.
tcs.TrySetException(args.Error);
return;
}
else
{
// Split the string into an array of words,
// then count the number of elements that match
// the search term.
string[] words = null;
words = args.Result.Split(' ');
string NAME = name.ToUpper();
int nameCount = (from word in words.AsParallel()
where word.ToUpper().Contains(NAME)
select word)
.Count();
// Associate the results with the url, and add new string to
the array
// that the underlying Task object will return in its Result
property.
results.Add(String.Format("{0} has {1} instances of {2}",
args.UserState, nameCount, name));
}
// If this is the last async operation to complete,
// then set the Result property on the underlying Task.
lock (m_lock)
{
count++;
if (count == urls.Length)
{
tcs.TrySetResult(results.ToArray());
}
}
};
#endregion
// Call DownloadStringAsync for each URL.
Uri address = null;
try
{
address = new Uri(urls[i]);
// Pass the address, and also use it for the userToken
// to identify the page when the delegate is invoked.
webClients[i].DownloadStringAsync(address, address);
}
catch (UriFormatException ex)
{
// Abandon the entire operation if one url is malformed.
// Other actions are possible here.
tcs.TrySetException(ex);
return tcs.Task;
}
}
// Return the underlying Task. The client code
// waits on the Result property, and handles exceptions
// in the try-catch block there.
return tcs.Task;
}
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<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
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, lo que no se
admite en la clase.
FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
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 usa en los ejemplos de la documentación para fines de demostración, no debe usarlo
en bucles paralelos a menos que sea necesario.
Ser consciente de los problemas de afinidad de los subprocesos
Algunas tecnologías, como la interoperabilidad COM para componentes STA (contenedor
uniproceso), 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 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<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:
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100)
.AsParallel()
.ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks
En este ejemplo, una 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 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 void button1_Click(object sender, EventArgs e)
{
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
});
}
En el siguiente ejemplo se muestra 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 void button1_Click(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
})
);
}
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 WithDegreeOfParallelism<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.
var 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 el operador de AsOrdered con este fin.
AsOrdered es distinto de AsSequential<TSource>. Una secuencia de AsOrdered todavía se
procesa en paralelo, pero se almacenan en búfer y se ordenan los resultados. Porque la
conservación del orden implica normalmente trabajo adicional, una secuencia de AsOrdered se
podría procesar más despacio que la secuencia predeterminada de AsUnordered<TSource>. 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.
evenNums = from num in numbers.AsParallel().AsOrdered() where num % 2 == 0
select num;
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<TSource> para ejecutar una consulta PLINQ. ForAll<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<TSource>. Se usa System.Collections.Concurrent.
ConcurrentBag<T> en este caso porque está optimizado para varios subprocesos que agregan
elementos simultáneamente sin intentar quitar ningún elemento.
var nums = Enumerable.Range(10, 10000);
var query = from num in nums.AsParallel()
where num % 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((e) => concurrentBag.Add(Compute(e)));
Cancelación
PLINQ se integra con los tipos de cancelación en .NET Framework 4. (Para obtener más
información, vea Cancelación en subprocesos administrados). 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<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 Operation
CanceledException.
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, pueden producir
algunas excepciones que no se encapsulan en AggregateException, y s para
ThreadAbortException tampoco se ajusta.
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.
int[] arr= ...;
Partitioner<int> partitioner = newMyArrayPartitioner<int>(arr);
var q = partitioner.AsParallel().Select(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.
3.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. Esas operaciones
son comunes en LINQ to Objects y PLINQ, y se prestan con facilidad a la programación en
varios subprocesos, por lo que se conocen como "perfectamente paralelas". 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 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).
var queryA = from num in numberList.AsParallel()
select ExpensiveFunction(num); //good for PLINQ
var queryB = from num in numberList.AsParallel()
where num % 2 > 0
select num; //not as good for PLINQ
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 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
ParallelExecution Mode.ForceParallelism a través del método WithExecutionMode<TSource>
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 (IE..
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.
3.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.
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<TSource>
devuelve las 10 primeras ciudades de la secuencia de origen que cumplen la condición.
var orderedCities = (from city in cities.AsParallel().AsOrdered()
where city.Population > 10000
select city)
.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<TSource>
para desactivarlo. En el siguiente ejemplo se consigue mediante la creación de dos consultas.
var orderedCities2 = (from city in cities.AsParallel().AsOrdered()
where city.Population > 10000
select city).Take(1000);
var finalResult = from city in orderedCities2.AsUnordered()
join p in people.AsParallel() on city.Name equals p.CityName into details
from c in details select new { Name = city.Name, Pop = city.Population, Mayor
= c.Mayor };
foreach (var city in finalResult) { /*...*/ }
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<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<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.
Resultado cuando la Resultado cuando la
Operador secuencia de origen está secuencia de origen no está
ordenada ordenada
Salida no determinista para Salida no determinista para
Aggregate operaciones no asociativas o no operaciones no asociativas o
conmutativas. no conmutativas.
All<TSource> No es aplicable No es aplicable
Any No es aplicable No es aplicable
AsEnumerable<TSource> No es aplicable No es aplicable
Salida no determinista para Salida no determinista para
Average operaciones no asociativas o no operaciones no asociativas o
conmutativas. no conmutativas.
Cast<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
Se devuelve el elemento
ElementAt<TSource> Elemento arbitrario
especificado
Se devuelve el elemento
ElementAtOrDefault<TSource> Elemento arbitrario
especificado
Except Resultados no ordenados Resultados no ordenados
Se devuelve el elemento
First Elemento arbitrario
especificado
Se devuelve el elemento
FirstOrDefault Elemento arbitrario
especificado
Ejecución no determinista en Ejecución no determinista en
ForAll<TSource>
paralelo 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
Se devuelve el elemento
Last Elemento arbitrario
especificado
Se devuelve el elemento
LastOrDefault Elemento arbitrario
especificado
LongCount No es aplicable No es aplicable
Min No es aplicable No es aplicable
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<TSource> no se almacena en búfer de forma predeterminada. Produce
inmediatamente todos los elementos de todos los subprocesos.
Como se muestra en el siguiente ejemplo, con el método WithMergeOptions<TSource>, puede
proporcionar a PLINQ una sugerencia que indica el tipo de combinación que se va a realizar.
var scanLines = from n in nums.AsParallel()
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
where n % 2 == 0
select ExpensiveFunc(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<TSource> None
Cast<TResult> None
Concat Consultas no ordenadas que solo tienen un origen de matriz o lista.
DefaultIfEmpty None
OfType<TResult> None
Reverse<TSource> Consultas no ordenadas que solo tienen un origen de matriz o lista.
Select None
SelectMany None
Skip<TSource> None
Take<TSource> None
Where None
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<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 contiene a operador como Reverse<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 para controlar las opciones de combinación depende del
tipo de la secuencia de origen y de si el operador AsOrdered se usó anteriormente en la consulta.
ForAll<TSource> siempre es NotBuffered; proporciona sus elementos inmediatamente.
OrderBy siempre es FullyBuffered; debe ordenar la lista completa antes de proporcionar
resultados.
3.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
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.
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, lo que la clase no
admite.
FileStream fs = File.OpenWrite(...);
a.Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));
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 System.Collections.Concurrent.ConcurrentBag<T>.
El mismo problema se aplica al Parallel.ForEach In Otros Words,
source.AsParallel().Where().ForAll(...) debe .forall (...) a
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 (contenedor
uniproceso), 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 Parallel.For,
Parallel.ForEach o ForAll<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:
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, ProcessorCount * 100).AsParallel().ForAll((j)
=>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks
En este ejemplo, una 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.
3.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<TSource>.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la PLINQ.
Ejemplo
var source = Enumerable.Range(100, 20000);
// Result sequence might be out of order.
var parallelQuery = from num in source.AsParallel()
where num % 10 == 0
select num;
// Process result sequence in parallel
parallelQuery.ForAll((e) => DoSomething(e));
// Or use foreach to merge results first.
foreach (var n in parallelQuery)
{
Console.WriteLine(n);
}
// You can also use ToArray, ToList, etc
// as with LINQ to Objects.
var parallelQuery2 = (from num in source.AsParallel()
where num % 10 == 0
select num).ToArray();
// Method syntax is also supported
var parallelQuery3 = source.AsParallel().Where(n => n % 10 == 0).
Select(n => n);
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., Este ejemplo está diseñado para mostrar el uso y podría
no ejecutarse más rápidamente que la consulta secuencial equivalente de LINQ to Objects.
3.2.7. Cómo: Controlar la ordenación en una consulta PLINQ
En estos ejemplos, se muestra cómo se controla el orden en una consulta PLINQ usando el
método de extensión AsOrdered.
Precaución
Estos ejemplos sirven principalmente para mostrar el uso y podría no ejecutarse más
rápidamente que la consulta secuencial equivalente de LINQ to 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.
var source = Enumerable.Range(9, 10000);
// Source is ordered; let's preserve it.
var parallelQuery = from num in source.AsParallel().AsOrdered()
where num % 3 == 0
select num;
// Use foreach to preserve order at execution time.
foreach (var v in parallelQuery)
Console.Write("{0} ", v);
// Some operators expect an ordered source sequence.
var lowValues = parallelQuery.Take(10);
procesamiento secuencial es por lo general más lento que el procesamiento en paralelo, a veces
es necesario para generar resultados correctos.
Precaución
Este ejemplo está diseñado para mostrar el uso y podría no ejecutarse más rápidamente que la
consulta secuencial equivalente de LINQ to Objects.
Ejemplo
En el siguiente ejemplo se muestra un escenario en el que se requiere AsSequential<TSource>,
a saber, para conservar el orden que se estableció en una cláusula anterior de la consulta.
// Paste into PLINQDataSample class.
static void SequentialDemo()
{
var orders = GetOrders();
var query = (from ord in orders.AsParallel()
orderby ord.CustomerID
select new
{
Details = ord.OrderID,
Date = ord.OrderDate,
Shipped = ord.ShippedDate
}).AsSequential().Take(5);
}
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.
static void PLINQExceptions_2()
{
var customers = GetCustomersAsStrings().ToArray();
// Using the raw string array here.
// First, we must simulate some currupt input
customers[54] = "###";
// 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.
Func<string[], string, bool> isTrue = (f, c) =>
{
try
{
string s = f[3];
return s.StartsWith(c);
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine("Malformed cust: {0}", f);
return false;
}
};
// Using the raw string array here
var parallelQuery = from cust in customers.AsParallel()
let fields = cust.Split(',')
where isTrue(fields, "C")
//use a named delegate with a try-catch
select new { city = fields[3] };
try
{
// We use ForAll although it doesn't really improve performance
class Program
{
static void Main(string[] args)
{
int[] source = Enumerable.Range(1, 10000000).ToArray();
CancellationTokenSource cs = 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(() =>
{
UserClicksTheCancelButton(cs);
});
int[] results = null;
try
{
results = (from num in
source.AsParallel().WithCancellation(cs.Token)
where num % 3 == 0
orderby num descending
select num).ToArray();
}
catch (OperationCanceledException e)
{
Console.WriteLine(e.Message);
}
class Program
{
static void Main(string[] args)
{
int[] source = Enumerable.Range(1, 10000000).ToArray();
CancellationTokenSource cs = 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(() =>
{
UserClicksTheCancelButton(cs);
});
double[] results = null;
try
{
results = (from num in
source.AsParallel().WithCancellation(cs.Token)
where num % 3 == 0
select Function(num, cs.Token)).ToArray();
}
catch (OperationCanceledException e)
{
Console.WriteLine(e.Message);
}
catch (AggregateException ae)
{
if (ae.InnerExceptions != null)
{
foreach (Exception e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
}
if (results != null)
{
foreach (var v in results) Console.WriteLine(v);
}
Console.WriteLine();
Console.ReadKey();
}
class aggregation
{
static void Main(string[] args)
{
// Create a data source for demonstration purposes.
int[] source = new int[100000];
Random rand = new Random();
for (int x = 0; x < source.Length; x++)
{
// Should result in a mean of approximately 15.0.
source[x] = rand.Next(10, 20);
}
// Standard deviation calculation requires that we first
// calculate the mean average. Average is a predefined
// aggregation operator, along with Max, Min and Count.
double mean = source.AsParallel().Average();
// We use the overload that is unique to ParallelEnumerable. The
// third Func parameter combines the results from each thread.
double standardDev = source.AsParallel().Aggregate(
// initialize subtotal. Use decimal point to tell
// the compiler this is a type double. Can also use: 0d.
0.0,
// do this on each thread
(subtotal, item) => subtotal + Math.Pow((item - mean), 2),
// aggregate results after all threads are done.
(total, thisThread) => total + thisThread,
// perform standard deviation calc on the aggregated result.
(finalSum) => Math.Sqrt((finalSum / (source.Length - 1)))
);
Console.WriteLine("Mean value is = {0}", mean);
Console.WriteLine("Standard deviation is {0}", standardDev);
Console.ReadLine();
}
}
}
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<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<T, TResult> es el valor de la expresión.
3.2.12. Cómo: Especificar el modo de ejecución en PLINQ
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
// Paste into PLINQDataSample class.
static void ForceParallel()
{
var customers = GetCustomers();
var parallelQuery = (from cust in customers.AsParallel()
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
where cust.City == "Berlin"
select cust.CustomerName)
.ToList();
}
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 esos casos, puede usar el método WithExecutionMode<TSource> y
especificar el valor ForceParallelism para indicar a PLINQ que ejecute siempre la consulta en
paralelo.
3.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.
namespace MergeOptions
{
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
class Program
{
static void Main(string[] args)
{
var nums = Enumerable.Range(1, 10000);
// Replace NotBuffered with AutoBuffered
// or FullyBuffered to compare behavior.
var scanLines = from n in nums.AsParallel()
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
where n % 2 == 0
select ExpensiveFunc(n);
Stopwatch sw = Stopwatch.StartNew();
foreach (var line in scanLines)
{
Console.WriteLine(line);
}
Console.WriteLine("Elapsed time: {0} ms. Press any key to exit.",
sw.ElapsedMilliseconds);
Console.ReadKey();
}
// A function that demonstrates what a fly sees when it watches
television
static string ExpensiveFunc(int i)
{
Thread.SpinWait(2000000);
return String.Format("{0}
*****************************************", i);
}
}
}
{
Console.WriteLine("You do not have permission to access one or more
folders
in this directory tree.");
return;
}
catch (FileNotFoundException)
{
Console.WriteLine("The specified directory {0} was not found.", path);
}
var fileContents = from file in files.AsParallel()
let extension = Path.GetExtension(file)
where extension == ".txt" || extension == ".htm"
let text = File.ReadAllText(file)
select new FileResult { Text = text , FileName = file };
//Or ReadAllBytes, ReadAllLines, etc.
try
{
foreach (var item in fileContents)
{
Console.WriteLine(Path.GetFileName(item.FileName) + ":" +
item.Text.Length);
count++;
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
Console.WriteLine(ex.Message);
return true;
}
return false;
});
}
Console.WriteLine("FileIteration_1 processed {0} files in {1}
milliseconds",
count, sw.ElapsedMilliseconds);
}
try
{
foreach (var item in fileContents)
{
Console.WriteLine(Path.GetFileName(item.FileName) + ":" +
item.Text.Length);
count++;
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
Console.WriteLine(ex.Message);
return true;
}
return false;
});
}
Console.WriteLine("FileIteration_2 processed {0} files in {1}
milliseconds",
count, sw.ElapsedMilliseconds);
}
Al usar GetFiles, asegúrese de que tiene permisos suficientes en todos los directorios del árbol.
De lo contrario, se producirá una excepción y no se devolverá ningún resultado. Al usar
EnumerateDirectories en una consulta PLINQ, es problemático controlar las excepciones de E/S
de una forma correcta que permita continuar con la iteración. Si el código debe controlar las
excepciones de E/S o de acceso no autorizado, debe considerar el enfoque descrito en Cómo:
Recorrer en iteración directorios con la clase paralela.
Si la latencia de E/S es un problema, por ejemplo, porque la E/S de archivos se produce a través
de una red, considere la posibilidad de usar una de las técnicas de E/S asincrónicas descritas en
TPL y la programación asincrónica tradicional de .NET y en esta entrada de blog.
3.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 de la consulta. Si necesita una medición más exacta, puede usar la propiedad
ElapsedTicks en lugar de ElapsedMilliseconds.
static void Main()
{
var source = Enumerable.Range(0, 3000000);
var queryToMeasure = from num in source
where num % 3 == 0
select Math.Sqrt(num);
Console.WriteLine("Measuring...");
// The query does not run until it is enumerated. Therefore, start the
timer here.
System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
// For pure query cost, enumerate and do nothing else.
foreach (var n in queryToMeasure) { }
long elapsed = sw.ElapsedMilliseconds; // or sw.ElapsedTicks
Console.WriteLine("Total query time: {0} ms", elapsed);
// Keep the console window open in debug mode.
#region DataClasses
public class Order
{
private Lazy<OrderDetail[]> _orderDetails;
public Order()
{
_orderDetails = new Lazy<OrderDetail[]>(() =>
GetOrderDetailsForOrder(OrderID));
}
public int OrderID { get; set; }
public string CustomerID { get; set; }
public DateTime OrderDate { get; set; }
public DateTime ShippedDate { get; set; }
public OrderDetail[] OrderDetails { get { return _orderDetails.Value;
} }
}
{
return System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) =>
line.StartsWith("CUSTOMERS") == false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END CUSTOMERS") ==
false);
}
};
}
return orderDetailStrings.ToArray();
}
}
Tipo Descripción
Permite que varios subprocesos trabajen en un
algoritmo en paralelo proporcionando un punto en el
System.Threading.Barrier
que cada tarea puede señalar su llegada y bloquearse
hasta que algunas o todas las tareas hayan llegado.
Simplifica los escenarios de bifurcación y unión
System.Threading.CountdownEvent proporcionando un mecanismo de encuentro
sencillo.
Primitiva de sincronización similar a
System.Threading. ManualResetEvent.
System.Threading.ManualResetEventSlim ManualResetEventSlim es un objeto ligero, pero solo
puede usarse en la comunicación que tiene lugar
dentro de un proceso.
Primitiva de sincronización que limita el número de
System.Threading.SemaphoreSlim subprocesos que pueden obtener acceso a la vez a un
recurso o grupo de recursos.
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
System.Threading.SpinLock 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.
Tipo pequeño y ligero que iterará en ciclos durante
System.Threading.SpinWait un período especificado y situará el subproceso en
estado de espera si el recuento de ciclos se supera.
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<T>.
En la tabla siguiente, se muestran los tipos de inicialización diferida:
Tipo Descripción
Proporciona una inicialización diferida ligera y segura para
System.Lazy<T>
subprocesos.
Proporciona un valor de inicialización diferida por cada
System.Threading.ThreadLocal<T> subproceso, donde cada subproceso invoca de forma
diferida la función de inicialización.
Proporciona métodos estáticos que evitan tener que asignar
una instancia de inicialización diferida dedicada. En su
System.Threading.LazyInitializer 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.
Boolean) 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
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<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.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Source must be array or IList.
var source = Enumerable.Range(0, 100000).ToArray();
// Partition the entire source array.
var rangePartitioner = Partitioner.Create(0, source.Length);
double[] results = new double[source.Length];
// Loop over the partitions in parallel.
Parallel.ForEach(rangePartitioner, (range, loopState) =>
{
// Loop over each range element without a delegate invocation.
for (int i = range.Item1; i < range.Item2; i++)
{
results[i] = source[i] * Math.PI;
}
});
Console.WriteLine("Operation complete. Print results? y/n");
char input = Console.ReadKey().KeyChar;
if (input == 'y' || input == 'Y')
{
foreach(double d in results)
{
Console.Write("{0} ", d);
}
}
}
}
Cada subproceso del bucle recibe su propio Tuple<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.
Para crear un particionador personalizado básico, derive una clase de
System.Collections.Concurrent. Partitioner<TSource> e invalide los métodos virtuales, tal y
como se describe en la siguiente tabla.
El subproceso principal llama a este método una vez y devuelve
IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle
GetPartitions
o la consulta puede llamar a GetEnumerator en la lista para
recuperar IEnumerator<T> de una partición distinta.
Devuelve true si implementa GetDynamicPartitions, de lo contrario,
SupportsDynamicPartitions
false.
Si SupportsDynamicPartitions es true, se puede llamar a este
GetDynamicPartitions
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<TSource> e invalide sus métodos virtuales
tal y como se describe en la siguiente tabla.
El subproceso principal llama a este método una vez y devuelve
IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle o
GetPartitions
la consulta puede llamar a GetEnumerator en la lista para recuperar
IEnumerator<T> de una partición distinta.
SupportsDynamic Devuelve true si implementa GetDynamicPartitions; de lo contrario,
Partitions falso.
GetDynamicPartitions Normalmente, solo llama a GetOrderableDynamicPartitions.
GetOrderable Si SupportsDynamicPartitions es true, se puede llamar a este método
DynamicPartitions 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<TSource>.
IList / matriz
IList / matriz con
Propiedad o método sin equilibrio IEnumerable
equilibrio de carga
de carga
Utiliza la creación de
Utiliza la creación de
Utiliza la particiones por
particiones por
creación de fragmentos
GetOrderablePartitions fragmentos y crea un
particiones optimizada para la
número de
por intervalos partitionCount
particiones estáticas.
especificada
Utiliza la creación de
Utiliza la creación de
particiones por
Produce una particiones por
OrderablePartitioner<TSource>. fragmentos
excepción no fragmentos creando
GetOrderableDynamicPartitions optimizada para las
admitida un número de
listas y las particiones
particiones dinámico.
dinámicas
KeysOrderedInEachPartition Devuelve true Devuelve true Devuelve true
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.
// An orderable dynamic partitioner for lists
class OrderableListPartitioner<TSource> : OrderablePartitioner<TSource>
{
private readonly IList<TSource> m_input;
IEnumerator IEnumerable.GetEnumerator()
{
return
((IEnumerable<KeyValuePair<long, TSource>>)this)
.GetEnumerator();
}
}
}
class ConsumerClass
{
static void Main()
{
var nums = Enumerable.Range(0, 10000).ToArray();
OrderableListPartitioner<int> partitioner =
new OrderableListPartitioner<int>(nums);
// Use with Parallel.ForEach
Parallel.ForEach(partitioner, (i) => Console.WriteLine(i));
// Use with PLINQ
var query = from num in partitioner.AsParallel()
where num % 2 == 0
select num;
foreach (var v in query)
Console.WriteLine(v);
}
}
// Solve for base given the area and the slope of the hypotenuse.
partitionLimits[i]=(int)Math.Floor(Math.Sqrt((2*area)/rateOfIncrease));
}
return partitionLimits;
}
class Consumer
{
public static void Main2()
{
var source = Enumerable.Range(0, 10000).ToArray();
Stopwatch sw = Stopwatch.StartNew();
MyPartitioner partitioner = new MyPartitioner(source, .5);
var query = from n in partitioner.AsParallel()
select ProcessData(n);
foreach (var v in query) { }
Console.WriteLine("Processing time with custom partitioner {0}",
sw.ElapsedMilliseconds);
var source2 = Enumerable.Range(0, 10000).ToArray();
sw = Stopwatch.StartNew();
var query2 = from n in source2.AsParallel()
select ProcessData(n);
foreach (var v in query2) { }
Console.WriteLine("Processing time with default partitioner {0}",
sw.ElapsedMilliseconds);
}
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.
3.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<TResult>, que crea
objetos Task<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.
Task taskA = Task.Factory.StartNew( () => ...);
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.
class Program
{
static CancellationTokenSource cts = new CancellationTokenSource();
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.
void QueueTasks()
{
// TaskA is a top level task.
Task taskA = Task.Factory.StartNew( () =>
{
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.
Task taskB = new Task( ()=> Console.WriteLine
("I was enqueued on the local queue."));
Task taskC = new Task(() => Console.WriteLine
("I was enqueued on the local queue, too."),
TaskCreationOptions.AttachedToParent);
taskB.Start();
taskC.Start();
});
}
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
.NET Framework 4 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 mantiene la situación de los datos. Esta arquitectura ayuda a que el equilibrio de carga de
ThreadPool en .NET Framework 4 trabaje más eficazmente que las versiones anteriores.
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.
3.7.1. Cómo: Crear un programador de tareas que limita el grado de
simultaneidad
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 TaskFactory.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.
namespace System.Threading.Tasks.Schedulers
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
class Program
{
static void Main()
{
LimitedConcurrencyLevelTaskScheduler lcts =
new LimitedConcurrencyLevelTaskScheduler(1);
TaskFactory factory = new TaskFactory(lcts);
factory.StartNew(()=>
{
for (int i = 0; i < 500; i++)
{
Console.Write("{0} on thread {1}", i,
Thread.CurrentThread.ManagedThreadId);
}
}
);
Console.ReadKey();
}
}
/// <summary>
/// Provides a task scheduler that ensures a maximum concurrency level
while
/// running on top of the ThreadPool.
/// </summary>
public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
/// <summary>Whether the current thread is processing work
items.</summary>
[ThreadStatic]
private static bool _currentThreadIsProcessingItems;
/// <summary>The list of tasks to be executed.</summary>
private readonly LinkedList<Task> _tasks = new LinkedList<Task>();
// protected by lock(_tasks)
/// <summary>The maximum concurrency level allowed by this scheduler
private readonly int _maxDegreeOfParallelism;
/// <summary>Whether the scheduler is currently processing work items.
private int _delegatesQueuedOrRunning = 0; // protected by
lock(_tasks)
/// <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 LimitedConcurrencyLevelTaskScheduler(int
maxDegreeOfParallelism)
{
if (maxDegreeOfParallelism < 1) throw new
ArgumentOutOfRangeException("maxDegreeOfParallelism");
_maxDegreeOfParallelism = maxDegreeOfParallelism;
}
/// <summary>
/// Informs the ThreadPool that there's work to be executed for this
scheduler
/// </summary>
private void NotifyThreadPoolOfPendingWork()
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
// 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)
{
Task item;
lock (_tasks)
{
// When there are no more items to be processed,
// note that we're done processing, and get out.
if (_tasks.Count == 0)
{
--_delegatesQueuedOrRunning;
break;
}
// Get the next item from the queue
item = _tasks.First.Value;
_tasks.RemoveFirst();
}
// Execute the task we pulled out of the queue
base.TryExecuteTask(item);
}
}
// We're done processing items on the current thread
finally { _currentThreadIsProcessingItems = false; }
}, null);
}
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace wpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private int fileCount;
int colCount;
int rowCount;
private int tilePixelHeight;
private int tilePixelWidth;
private int largeImagePixelHeight;
private int largeImagePixelWidth;
private int largeImageStride;
PixelFormat format;
BitmapPalette palette;
public MainWindow()
{
InitializeComponent();
// For this example, values are hard-coded to a mosaic of 8x8
tiles.
// Each tile is 50 pixels high and 66 pixels wide and 32 bits per
pixel.
colCount = 12;
rowCount = 8;
tilePixelHeight = 50;
tilePixelWidth = 66;
largeImagePixelHeight = tilePixelHeight * rowCount;
largeImagePixelWidth = tilePixelWidth * colCount;
largeImageStride = largeImagePixelWidth * (32 / 8);
this.Width = largeImagePixelWidth + 40;
image1.Width = largeImagePixelWidth;
image1.Height = largeImagePixelHeight;
}
Comentarios
En este ejemplo se muestra cómo mover los datos del subproceso de la interfaz de usuario,
modificarlos usando bucles paralelos y objetos Task y, a continuación, devolverlos a una tarea
que se ejecuta en el subproceso de la interfaz de usuario. Este enfoque es útil cuando se tiene
que utilizar Task Parallel Library para realizar operaciones admitidas o no admitidas por la API
de WPF, o que no son suficientemente rápidas. Otra manera de crear un mosaico de la imagen
en WPF es utilizar un objeto WrapPanel y agregarle las imágenes. WrapPanel controlará el
trabajo de colocar los mosaicos. Sin embargo, este trabajo solo se puede realizar en el
subproceso de la interfaz de usuario.
Este ejemplo tiene algunas limitaciones. Solo se admiten imágenes de 32 bits por píxel, por
ejemplo; el objeto BitmapImage daña las imágenes de otros formatos durante la operación de
cambio de tamaño. Además las imágenes de origen deben ser mayores que el tamaño del
mosaico. Como un ejercicio más extenso, puede agregar la funcionalidad para controlar varios
formatos de píxel y tamaños de archivo.
3.8. Expresiones lambda en PLINQ y TPL
La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas)
contiene muchos métodos que toman una de las familias de delegados System.Func<TResult> o
System.Action como parámetros de entrada. Estos delegados se usan para pasar la lógica del
programa personalizado al bucle, tarea o consulta paralelo. Los ejemplos de código de TPL, así
como PLINQ, usan expresiones lambda para crear instancias de los delegados como bloques de
código alineado. En este tema se proporciona una breve introducción a Func y Action, y se
muestra cómo usar las expresiones lambda en la biblioteca TPL y PLINQ.
Delegado Func
Un delegado Func encapsula un método que devuelve un valor. En una signatura de Func, el
último parámetro de tipo, o el situado en el extremo derecho, siempre especifica el tipo de valor
devuelto. Una causa común de los errores del compilador es el intento de pasar dos parámetros
de entrada a System.Func<T, TResult>; de hecho, este tipo toma un único parámetro de entrada.
La biblioteca de clases de .NET Framework define 17 versiones de Func:
System.Func<TResult>, System.Func<T, TResult>, System.Func<T1, T2, TResult>, y así
sucesivamente hasta System.Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
T14, T15, T16, TResult>.
Delegado Action
Un delegado System.Action encapsula un método (Sub en Visual Basic) que no devuelve
ningún valor o que devuelve void. En una signatura de Action, los parámetros de tipo solamente
representan parámetros de entrada. Al igual que sucede con Func, la biblioteca de clases de
.NET Framework define 17 versiones de Action, desde una versión que no tiene ningún
parámetro de tipo hasta una versión con 16 parámetros de tipo.
Ejemplo
En el ejemplo siguiente del método Parallel.ForEach<TSource,
TLocal>(IEnumerable<TSource>, Func<TLocal>, Func<TSource, ParallelLoopState, TLocal,
TLocal>, Action<TLocal>) se muestra cómo expresar los delegados Func y Action mediante
expresiones lambda.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class ForEachWithThreadLocal
{
// Demonstrated features:
// Parallel.ForEach()
// Thread-local state
// Expected results:
// This example sums up the elements of an int[] in parallel.
// Each thread maintains a local sum. When a thread is initialized,
that
// local sum is set to 0.
// On every iteration the current element is added to the local sum.
// When a thread is done, it safely adds its local sum to the global
sum.
// After the loop is complete, the global sum is printed out.
// Documentation:
// http://msdn.microsoft.com/en-us/library/dd990270(VS.100).aspx
static void Main()
{
// The sum of these elements is 40.
int[] input = { 4, 1, 6, 2, 9, 5, 10, 3 };
int sum = 0;
try
{
Parallel.ForEach(
input, // source collection
() => 0, // thread local initializer
(n, loopState, localSum) => // body
{
localSum += n;
Console.WriteLine("Thread={0}, n={1}, localSum={2}",
Thread.CurrentThread.ManagedThreadId, n, localSum);
return localSum;
},
(localSum) => Interlocked.Add(ref sum, localSum)
);
Console.WriteLine("\nSum={0}", sum);
}
// No exception is expected in this example, but if one is still thrown from
a task,
// it will be wrapped in AggregateException and propagated to the main
thread.
catch (AggregateException e)
{
Console.WriteLine("Parallel.ForEach has thrown an exception.
THIS WAS NOT EXPECTED.\n{0}", e);
}
}
}