Primera parte del libro de asincronía para .NET, pasa de 0 a 100 en un pequeño libro ;)
Experto en:
Asincronía con .NET
Ver. 1.0.0
Bienvenidos
Acerca de…
¡Hola! Gracias por leer
“Experto en: Asincronía con .NET”.
Espero poder aportarte los conocimientos mínimos y necesarios
para que puedas iniciarte en este apasionante mundo.
Jose María Flores Zazo, autor
Licencia bajo…
Creative Commons
Atribución 4.0 Internacional
¡Esta es una Licencia de Cultura Libre!
Apúntate a la comunidad
https://jmfloreszazo.com/azure-iot-esp/
Índice
Resumen
Sección 1
Introducción
Resumen y herramientas que
debes manejar
Sección 2
Threading & Task
Thread, ThreadPool y Task.
¿Legacy?
Sección 4
Concurrencia y Sincronización
Conceptos y algunos ejemplos
Sección 5
Estructuras de Datos
Un repaso a las estructuras
asíncronas y algún bonus más,
Sección 3
Async
Conceptos básicos, medios y
avanzados.
Índice
Resumen
Requisitos previos y herramientas
Resumen
Conocimientos sobre
codificación en C#
Entorno de desarrollo
Visual Studio 2022
Practicar, practicar,
practicar…
Sección 1
Introducción
¿Qué vamos a ver?
Introducción
No es necesario que conozcas C# a nivel muy avanzado pero tampoco te será fácil si eres un novato.
Lo que vamos a ver es:
• Algunas herramientas útiles como BenchmarkDotNet, ConcurrencyVisualizer, Parallel Stacks y Parallel Tasks.
• Threading: threads & Thread pool.
• Async:
− Task, ValueTask, async await, …
− Context, executions, fire & forget pattern, deadlocks, awaitable, TaskCompletionSource, Thread Local, …
− Aggregations, Parallel.For, Task.WhenAll, cancellation, IAsyncDisposable, …
• Concurrencia de bajo nivel.
• Herramientas para concurrencia, como locks, lockings, monitor, wait handles, deadlock debugging, …
• Estructuras de datos concurrentes.
• Y algunas cosas mas.
Son temas muy áridos que espero puedas entender con ejemplos sencillos.
Pero para comenzar os voy a mostrar una serie de herramientas que nos ayudaran desde el principio.
Resumen – De un vistazo
BenchmarkDotNet
Herramientas
Seguro que existen otras herramientas, pero estas son las que se suelen ver cuando vemos las comparativas entre .Net
Core 3.1 y .Net6, por ejemplo.
En este enlace tienes toda la documentación y una cantidad de ejemplos suficiente como para poder llegar a tener un
control de la misma:
https://benchmarkdotnet.org/articles/overview.html
Y si no, tambien tienes un libro dedicado exclusivamente a esta herramienta:
NuGet – Para benchmarking
Ejemplo
BenchmarkDotNet(1/2)
Lo más sencillo es comenzar con un ejemplo de línea de aplicación de consola para que puedas ver como funcia a muy
alto nivel.
Para ello vamos a lanzar el siguiente ejemplo: BenchmarkDotNetSample
NuGet –https://www.nuget.org/packages/BenchmarkDotNet
Ejemplo
BenchmarkDotNet(2/2)
Y ejecutamos, recuerda que para poder obtener una medición coherente debes ejecutar en modo build y no en modo
debug:
Y obtendrás una cantidad importante de información en la consola, pero nos quedamos con esta:
Lo que más te debe importar de todo esto son los decoradores que hacen que los test funcionen y midan lo que tu
quieras, esto está todo en la documentación:
ConcurrencyVisualizer
Herramientas
Esta extensión que lleva ya tiempo en el mercado, nos ayudará a visualizar la simultaneidad para ver como funciona la
aplicación multiproceso.
https://docs.microsoft.com/es-es/visualstudio/profiling/concurrency-visualizer?view=vs-2022
En este enlace tienes un artículo básico y el ejemplo ConcurrencyVisualizer.
https://jmfloreszazo.com/visualizador-de-concurrencia-para-visual-studio-2022
Extensión – Para Visual Studio 2022
Ejemplo Parallel Stacks/Tasks
Parallel Stacks/Tasks(1/4)
A continuación os pongo un recordatorio de las herramientas que nos proporciona Visual Studio 2022 para poder
depurar paralelismo. Seguro que ya lo conocéis pero es bueno pararse 10 minutos para recordar su funcionamiento.
Con el ejemplo ParallelStacksTasks, podrás probar según estos pasos:
Debug – Parallel
Ejemplo Parallel Stacks/Tasks
Parallel Stacks/Tasks(2/4)
Aquí podrás ver los Threads:
Ejemplo Parallel Stacks/Tasks
Parallel Stacks/Tasks(3/4)
El Parallel Stack, cuanto más subprocesos (hijos, por ejemplo) en paralelo tengas más información verás en el
diagrama:
Ejemplo Parallel Stacks/Tasks
Parallel Stacks/Tasks(4/4)
Y para finalizar el Parallel Watch:
From0to100NetAsync – Repositorio
https://github.com/jmfloreszazo/From0to100NetAsync
Ejemplos
GitHub
Sección 2
Threading & Task
Teoría
Threads(1/25)
Un Thread (usaré indistintamente ingles y español) es una unidad de ejecución a nivel de sistema operativo. En la
Wikipedia tenéis una entrada con una definición formal, la anterior es un resumen mío.
La separación de un programa en múltiples tareas, es lo que se conoce como multitarea. Que nos permite usar, por
ejemplo, varios programas, ya que el procesador es capaz de gestionar múltiples tareas al mismo tiempo. Esto se
traduce en una mejora notable del rendimiento ya que no depende directamente de la velocidad de la CPU.
El propósito de los threads es dividir un proceso. Un programa puede estar formado de uno o varios procesos, que a su
vez, se dividen en hilos. Los hilos de un proceso comparte comandos, código y acceder al sistema de forma simultanea
para completar las tareas lo más rápido posible.
A veces me encuentro con una confusión entre hilos y núcleos del procesador. Los hilos y los múltiples núcleos ayudan
a que el PC trabaje más rápido, pero son cosas muy diferentes. Los hilos existen a nivel de software mientras que los
núcleos son físicos. Los hilos comparten recursos del sistema dentro de un mismo núcleo.
Puedes pensar que los hilos son maravillosos, pero tambien tiene alguna desventaja. Como por ejemplo que múltiples
hilos pueden interferir entre ellos, sobre todo cuando comparten recursos como cachés. Y por desgracia algunos
procesadores limitan el multihilo gestionado por hardware.
¿Qué son los Threads? –¿Qué son los hilos?
Teoría
Threads(2/25)
Cuando confrontamos hilos y hardware tenemos estas tres posibles opciones:
• Una sola CPU
• Múltiples CPUs
• Hyper-Threading (Intel) y SMT (AMD, simultaneous multithreading)
Cada uno de ellos tendrá sus detalles que iremos viendo en esta sección. A nivel hardware no voy a profundizar, no
tiene sentido hablar a este nivel, lo importante es que se entienda a un alto nivel y para nuestro día a día de desarrollo.
Tambien veremos un ejemplo del funcionamiento de un sola CPU, del funcionamiento del funcionamiento de los hilos.
Teoría
Threads(3/25)
Cuando tenemos varias CPUs sin multicore:
CPU #1
T1
CPU #2
T2
Teoría
Threads(4/25)
Cuando tenemos varias CPUs con multicore:
CPU #1
T2
Core#2
T1
Core#1
Debido a la arquitectura de los microprocesadores, la
CPU tiene que organizar los tiempos para que puedan
trabajar los cores. Esto se traduce en tiempo perdidos
en los cores.
Teoría
Threads(5/25)
Cuando tenemos el trabajo en Hyper-Threading:
CPU #1
T2
T1
En realidad los dos hilos estan trabajando en un solo
core, por tanto esta intercalando el uso de los hilos en
ese mismo Core HT.
Core HT
Teoría
Threads(6/25)
Veamos como funciona junto a la CPU para manejar los hilos:
T1 T2
T3
T4 T5 Tn
CPU
Memoria
Teoría
Threads(7/25)
CPU
T1
T2
T3
T5 Tn
Memoria
T1 contexto
Registro CPU, state,
TLS, …
Creación
del contexto
T4
Teoría
Threads(8/25)
CPU
T2
T3
T5 Tn
Memoria
T1 contexto
Registro CPU, state,
TLS, …
T1
T4
Teoría
Threads(9/25)
CPU
T2
T3
T5 Tn
Memoria
T1 contexto
Registro CPU, state,
TLS, …
T1
T2 contexto
Registro CPU, state,
TLS, …
T4
Teoría
Threads(10/25)
CPU
T1
T3
T4 T5 Tn
Memoria
T2 contexto
Registro CPU, state,
TLS, …
T2
T1 contexto
Registro CPU, state,
TLS, …
Cambio de
contexto
Teoría
Threads(11/25)
¿Qué ocurre cuando tenemos muchos hilos y una sola CPU?
• Nos toca mantener un contexto de hilo (thread context) :
− CPU registrada (incluida la IP).
− Modo actual (Kernel/Usuario).
− Dos pilas (Kernel/Usuario).
− La persistencia local del hilo.
− Prioridad.
− Estado.
− ….
• Lo pagamos caro con el cambio de contexto, muchos ciclos de reloj y limpieza de cache.
• Crear hilos en este caso ya es caro de por si, no tenemos más CPU o Cores que ayuden.
• Debemos decidir como deben ejecutarse los hilos:
− Al azar o con turnos.
− Desarrollar un sistema personalizado.
• Y entra en juego el programador de hilos:
− Distinto a nivel de SO, Windows distinto a Linux e incluso entre versiones.
− A nivel de todo el sistema todos los subprocesos estarán en el mismo grupo de programación.
− Es preventivo: activa o desactiva subprocesos de la CPI en cualquier momento.
− El proceso se ejecuta durante un período de tiempo llamado quantum. Se ejecuta el máximo establecido.
− Puede se reemplazado por un subproceso de mayor prioridad o que este basado en prioridad: siempre se
ejecuta al menos uno subproceso.
Teoría
Threads(12/25)
¿Quantum?
Si nos vamos a la siguiente opción del Sistema Operativo Windows (en Linux no sabría deciros), podremos ver dos
opciones:
Teoría
Threads(13/25)
• Programas, la opción por defecto:
− Variable quantum, pequeña.
− Ciclos de reloj muy pequeños.
− Más fluido.
• Servicios en background:
− Variable quantum, más grande.
− Ciclos de reloj más grandes.
− Baja el coste de hacer cambio de contexto.
• Si estas en Windows Server o una VM que necesite trabajos muy largos, la segunda opción sería la adecuada.
Hace muchos años en Windows 2000 era muy útil entrar en el registro del sistema y cambiar una serie de valores en las
claves, que no voy a mostrar, ya que llevo mucho tiempo desconectado de este tipo de cosas y no se como se
comporta esto en los nuevo servidores o en un Windows 10, por ejemplo.
Entendiendo la prioridad de los hilos en Windows
En .NET tenemos:
• ProcessPriorityClass para procesos: idle, below normal, normal (default), above normal, high y realtime.
• ThreadPriority para hilos: lowest, below normal, normal (default), above normal, highest.
Teoría
Threads(14/25)
Como lo usaríamos:
Process p = Process.GetCurrentProcess();
p.PriorityClass = ProcessPriorityClass.Hight
Thread t = . . .;
t.Priority = ThreadPriority.Highest;
Como estamos con hilos, veamos algo más sobre como deja de funcionar la prioridad forzada:
• Aumentar prioridad, temporalmente. Conocido como Priority Boost:
− Un hilo que no se ejecuta durante algún tiempo, evita que se lance aunque tenga establecido el incicio con
prioridad.
− Un hilo que sea propietario de bloqueos (compartidos o exclusivos), evita que se inicie aunque tenga eventos
programados o bloqueado el inicio.
− Finalización de I/O, para hilos de I/O o UI, procesado de mensajes de hilos de Windows, por ejemplo.
• Y muchas más acciones:
− Multimedia: viendo una película y tu programa esta en alta prioridad.
− Cuando tienes un juego cargado en memoria y tu programa tiene puesta alta prioridad.
− ...
Teoría
Threads(15/25)
Estado de los hilos
• Ready: se ejecutará inmediatamente (pero probablemente el hardware lo gestionará).
• Running: ejecutándose en la CPU (hasta que el quantum lo diga).
• Waiting: esperando a la ejecución (esperando por algo, fue suspendido, …).
• Standby, Terminanted, Inizialized, Stopped, … ya lo iremos viendo.
Aquí tenéis un ejemplo que muestra los estados: ThreadStatesSample.
Ready
Running Waiting
postponed
Quatum ended
preemption
Hardware thread
assigned
Postponed
ended
Teoría
Threads(16/25)
Continuando con el ejemplo anterior, extrapolemos a los hilos:
CPU
T1
T2
T3
T4 T5 Tn
Teoría
Threads(17/25)
CPU
T1
T2
T3
T4
T5
Tn
Teoría
Threads(18/25)
CPU
T1
T3
T4
T5
Prioridad Hilo
Tiempo
Teoría
Threads(19/25)
CPU
T1
T3
T4
T5
Prioridad Hilo
Tiempo
T1
Teoría
Threads(20/25)
CPU
T1
T3
T4
T5
Prioridad Hilo
Tiempo
T1
quantum
quantum
Continua ejecutándose
debido a que es la
máxima prioridad…
Teoría
Threads(21/25)
CPU
T1
T3
T4
T5
Prioridad Hilo
Tiempo
T1
quantum
quantum
T1
, comienza a esperar,
tras su finalización del
quantum…
T1
Teoría
Threads(22/25)
CPU
T1
T3
T4
T5
Prioridad Hilo
Tiempo
T1 T1
Teoría
Threads(23/25)
CPU
T1
T3 T4
T5
Prioridad Hilo
Tiempo
T1
quantum
T1
, tiene la máxima
prioridad y deja de
esperar… incluso antes
de que T4
termine…
T1
quantum
T4
Teoría
Threads(24/25)
CPU
T4
T3 T1
T5
Prioridad Hilo
Tiempo
T1
T1
, ejecutándose aunque
T4
se había adelantado…
T1 T4 T1
Core #1
Tiempo
T1 T1 T4 T1
Core #2
Core #3
Core #4
T2 T3 T3 T4
T5 T4 T4 T5
T6 T6 T4 T4
Teoría
Threads(25/25)
Ciclo de Vida de los Threads
Resumen esquemático
Stop
Running Suspended
Interrupt
Aborted
Wait
Sleep
Join
Wait/Sleep/Join
Abort
Start
Abort
Suspend
Resume
Abort
Teoría
Threads en .Net(1/10)
Tipos de hilos:
• Hilos nativos, son los que proporciona el sistema operativo.
• Hilos no administrados, ejecutan codigo no administrado:
− Obviamente nativo.
− Incluido codigo de runtime de .NET (como puede ser GC, Garbage Collection).
− No se suspenden durante el GC.
• Hilos administrados, ejecutan código de .NET como los de nuestra aplicación C#:
− En su mayoría siguen siendo hilos nativos.
− Hilos administrados que llama a codigo no administrado (Invokes), sigue siendo administrados.
− Mantienen un contexto mayor que los hilos nativos .
• Hilos especiales:
− Hilo de finalización.
− Hilos de GC.
− Hilos de debugger.
Primer plano vs segundo plano:
• La aplicación espera hasta que los hilos en segundo plano finalicen.
• Cuando una aplicación existe, todos los hilos de segundo plano forzosamente se detienen:
− Cuidado con la limpieza, Dispose se ignorará y los bloques Finally tambien.
− Cuidado con los cálculos, se perderán…
− Solución: ¿dejamos que un hilo de primer plano espere a los del segundo?.
− Y sí… los ThreadPools utilizan hilos de segundo plano.
Antes hemos visto la teoría general –Ahora nos toca ver que ocurre en .Net
Teoría
Threads en .Net(2/10)
“Tan pronto como escribas new Thread(), se acabó;
tu proyecto ya tiene codigo legacy”
Concurrency in C# Cookbook, 2nd Edition
Aun así… nos permite explicar algunos conceptos básicos.
Teoría
Threads en .Net(3/10)
Thread.Start & Thread.Join
var t = new Thread(…); // Solo gestionada, aun no hay nativo
t.IsBackground = true; // Configura el hilo de segundo plano
t.Start(arg); // Nativo asignado e iniciado
t.join(); // El hilo actual espera a que el hilo termine (bloqueo)
Podemos usar la rutina con un solo parámetro o no:
public delegate void ThreadStart();
public Thread (System.Threading.ThreadStart start);
public delegate void ParametrizedThreadStart(object obj);
public Thread (System.Threading.ThreadStart start);
Teoría
Threads en .Net(4/10)
Thread Exception Handling
Una excepción lanzada desde un hilo terminará con toda la aplicación:
El ejemplo esta en la solución: ThreadsExceptionKillAllSample , proyecto: ThreadsException
Teoría
Threads en .Net(5/10)
Aunque no previene que la aplicación muera, al menos podemos hacer algo de trazabilidad:
El ejemplo esta en la solución: ThreadsExceptionKillAllSample , proyecto: ThreadsExceptionWithLog
Teoría
Threads en .Net(6/10)
Thread.Sleep
Supongo que esto lo habrás usado cientos de veces para parar un rato la ejecución en algún tipo de pruebas. Aun así
vamos a ver como funciona.
Para ello lo primero es bajar esta aplicación y probar la resolución del reloj de tu sistema: ClockRes v2.1
Despegada esta duda, vamos a ver algunas características de Thread.Sleep:
• El sistema operativo no programará la ejecución del hilo actual durante le tiempo especificado…
− Debes ponerlo en esperar con WaitSleepJoin.
− O retomarlo usando un temporizador cambiando el estado a Ready.
• El sistema operativo no programará la ejecución del hilo actual durante le tiempo especificado…
− Ya que el tiempo es limitado para la ejecución, por defecto 15,6ms aproximadamente.
− O por qué algunos programas como (SQL Sever o Chrome) son capaces de modificarlo.
• Obviamente esto impacta en la programación de los hilos.
Teoría
Threads en .Net(7/10)
Thread.Sleep & Thread.Yield & Thread.SpinWait
Si ejecutas esto va a dejar la CPU bloqueada al 100% en el proceso:
while (!condición) { . . . Tu Código . . . };
Pero podemos dejar algunos espacios para que se procesen otros hilos:
while (!condición)
{
. . . Agregar código que te permita ir a otros procesos . . .
};
Aquí tienes algunas opciones o combinaciones de ellas que tambien nos ayudan:
Thread.Sleep(0); // 0ms
Thread.Sleep(100); // 100ms
Thread.Yield(); // ns, selected time by SO
Thread.SpinWait(10); // ?
Yield, cede la ejecución a otro subproceso mientras que Sleep suspende el proceso durante un tiempo. Y SpinWait
lo que hace es que espere un número definido de iteraciones definidas en el parámetros. Ahora lo vermeos mejor.
Teoría
Threads en .Net(8/10)
Al final usar: Thread.Sleep(100);
Fuerzas al contexto a cambiar de subproceso limitándolo en realidad a la resolución de tiempo que marcas en SO.
Usar: Thread.Sleep(0);
Lo que estas haciendo es marcar al subproceso como Ready, con el funcionamiento ya conocido en la teoría.
Usar: Thread.Yield();
Es muy rápido y gestiona bien los subprocesos Ready, pero solo lo que están en la misma CPU. Esta instrucción es una
buena instrucción para diagnosticar tu programa ya que puede romper o arreglar tu programa.
Usar: Thread.SpinWait(10);
Llama X veces a una instrucción especial de la CPU para ejecutar la espera. X normalmente son iteraciones del ciclo del
la CPU.
Teoría
Threads en .Net(9/10)
Thread.Abort & Thread.Interrupt
Lanzas una excepción ThreadAbortException:
Thread.Abort;
¡En .NET5 en adelante ya no está soportado! Ver información.
Lanza una excepción ThreadInterruptedException, incluso cuando estas en WaitSleepJoin (sí, ya que el hilo
esta en la memoria con su correspondiente contexto):
Thread.Interrupt
Aquí tienes un ejemplo.
Thread Cancelaciones
En el blog ya puse un ejemplo de .NET, ver artículo. Revísalo para tener más contexto de esto.
Teoría
Threads en .Net(10/10)
Thread Coordinación Simple.
Existe una forma de resetear un evento con las siguientes instrucciones:
• AutoResetEvent, manda una señal al hilo para que lo cierre (resetea).
• ManualResetEvent/ManualResetEventSlim, son señales para cerrar muchos hilos.
La señal se comparte entre diferentes hilos:
var autoEvent = new AutoResetEvent(false);
autoEvent.WaitOne(); // Uno o más eventos esperan que la señal se active
autoEvent.Set(); // Una vez terminado el trabajo se deben asignar las señales
autoEvent.Close(); // o autoEvent.Dispose() no te olvides de limpiar.
Teoría y Práctica
ThreadsPool en .Net(1/14)
¿Qué son los ThreadsPool? –¿Qué son los grupos de subprocesos/hilos/threads?
Un grupo de subprocesos (thread pool) es un patrón de diseño de software que se usa para lograr concurrencia en la
ejecución. A menudo, tambien llamado modelo de trabajadores replicados (replicated worker).
Un grupo de subprocesos mantiene varios subprocesos en espera de que el programa supervisor asigne tareas para su
ejecución simultánea. Al mantener un conjunto de subprocesos, el modelo aumenta en rendimiento y evita la latencia
en la ejecución debido a la frecuente creación y destrucción de subprocesos de corta duración.
La cantidad de subprocesos disponible se ajusta según los recursos del hardware y en una cola de tareas paralelas.
Task Queue
Thread Pool
Completed Tasks
Teoría y Práctica
ThreadsPool en .Net(2/14)
Las características principales son:
• Que los pools pueden ser de dos tipos:
− Worker Threads, que ejecutan elementos de trabajo vía callbacks, callbacks de timer, y esperan a registros de
callbacks.
− Subproceso de I/O.
• Que usan hilos de background.
• Es un grupo único de todo el proceso.
• Internamente mucho servicios lo usan como WCF, ASP.NET, temporizadores, PLINQ, patrón asíncrono basado en
eventos (EAP, event-bases asynchronous pattern) e incluso el JIT a varios niveles.
• Tiene un mecanismo de auto-balanceo, tratando de mantener una buena cantidad de subprocesos.
• No tiene soporte prioritario de los elementos de trabajo, es decir, nos toca escribirlo nosotros mismos.
• Restablece las propiedades de subprocesos devuelto al grupo.
Como cualquier otra librería de .Net los ThreadPool tienen sus definiciones y vamos a ver las más habituales:
• Siendo la más utilizada: ThreadPool.QueueUserWorkItem
• En menor medida: ThreadPool.GetAviableThreads, ThreadPool.GetMaxThreads,
ThreadPool.SetMaxThreads, ThreadPool.GetMinThreads, ThreadPool.SetMinThreads
• Y casi nunca, por su grado de complejidad: ThreadPool.RegisterWaitForSingleObject,
ThreadPool.BindHandle, ThreadPool.UnsafeQueueNativeOverlapped,
ThreadPool.UnsafeQueueUserWorkItem, …
Teoría y Práctica
ThreadsPool en .Net(3/14)
ThreadPool.QueueUserWorkItem
Pone un elemento de trabajo en una cola (delegado) para que lo ejecute en algún momento de los subprocesos de
trabajo del ThreadPool:
• No garantiza que se ejecutará inmediatamente.
• No hay una forma directa de esperarlo u obtener un resultado (fire & forget).
• Tiene algunas peculiaridades que veremos poco a poco.
El siguiente ejemplo muestra como lo usamos y con un manejador de excepciones, si no, no se podrá manejar y el
programa aunque se sigue rompiendo puedes hacer algo antes de cerrar: ThreadPoolQueueUserWorkItemSample
Cuando un ThreadPool se termina:
• Recuerda que son subprocesos en background.
• .Net no esperará a que termine el trabajo, incluidos aquellos que estan en la cola.
• Es conveniente cooperar y usar CancellationToken.IsCancellationRequested.
Logicamente si queremos asegurarnos que se procesa hasta el final considera: usa un mecanismo de hilos separados
con algo de sincronización. Y otra es la que veremos a continuación.
Teoría y Práctica
ThreadsPool en .Net(4/14)
Crear los hilos en background ya que usar IsBackground resetea el hilo y obviamente te juntas con otras dos
situaciones:
• El hilo principal de la aplicación puede finalizar antes que los subprocesos se iniciaran. Por tanto es recomendable
usar ThreadPool.PendingWorkItemCount en otro hilo de background. Se complica por momentos.
• Y si un evento esta encolado, el DoSomeWork podría terminar antes de establecer IsBackgroud. Por tanto
necesitas un mecanismo de programación o sincronización. Otro problema más.
Ahora es cuando llegamos a un punto importante: La administración del pool.
Teoría y Práctica
ThreadsPool en .Net(5/14)
Administración del Pool
• Un pool comienza un número de hilos por defectos.
• Después necesitará crear o eliminar algunos de esos hilos:
− Intentará mantener el numero de hilos bajo, modificando la carga de trabajo y evitando crear nuevos hilos
tanto tiempo como le sea posible.
− Si tiene poco hilos y varios hilos estan en espera, significa un gasto de CPU innecesario.
− Si tiene muchos hilos y esta continuamente cambiado de contexto se esta tirando a la basura proceso de la
CPU.
• Esto nos lleva a preguntarnos ¿cual es el ratio correcto para la carga de trabajo?, un thread pool mantiene un mínimo
y un máximo de hilos.
Máximo número de hilos:
• Es un valor real cuantitativo.
• No podrá crear más de los indicados.
• Si se excede del número el mecanismo que aplica es encolarlos y usarlos cuando tenga hilos disponibles.
Mínimo número de hilos:
• En realidad no es un mínimo, ya que puede ser el valor 0.
• Si existen menos hilo que el mínimo: se crean inmediatamente para alcanzar ese valor.
• Si tenemos más hilos que el mínimo, irá creando hilos a intervalos pequeños.
Teoría y Práctica
ThreadsPool en .Net(6/14)
Configuración
La configuración se realiza mediante un fichero, por ejemplo [appname].runtimeconfig.json o el fichero de
proyecto, en el siguiente enlace podrás ampliar toda la información:
JSON
{
"runtimeOptions": {
"configProperties": {
"System.Threading.ThreadPool.MinThreads": 4
}
}
}
Proyect:
4
Desde programación tambien se puede: ThreadPool.SetMinThread, ThreadPool.SetMaxThreads, …
Teoría y Práctica
ThreadsPool en .Net(7/14)
Esperas
Registra un delegado para que espere a la clase WaitHandle mediante un tiempo en ms. Aquí tenéis un ejemplo.
Por ejemplo: ThreadPool.RegisterWaitForSingleObject espera a un handle en vez de bloquear todo el hilo con
.WaitOne().
Colas
Una carga de trabajo se lanzará a de estas dos colas:
• Global task queue:
− Si el elemento se encola desde un sitio que no es un hilo al thread pool.
− Si se usa ThreadPool.QueueUserWorkItem o ThreadPool.UnsafeQueueUserWorkItem.
− Si se usa TaskCreationOption.PreferFairness con la ocpión Task.Factory.StarNew.
− Si se llama a Task.Yield en el subproceso por defecto.
• Tareas locales encoladas por cada worker thread:
• Si un elemento a sido encolado de un thread pool thread.
Esto nos lleva a desencolar: se desencola usando LIFO para colas locales (si creas colas locales dentro de otro hilo usa
FIFO) y la colas globales usa FIFO.
Teoría y Práctica
ThreadsPool en .Net(8/14)
Desencolado
El desencolado puede producir un deadlock, en este artículo lo explican muy bien: .NET ThreadPool starvation, and how
queuing makes it worse
En resumen:
1. llega una ráfaga constante de peticiones.
2. el pool de hilos se queda sin recursos.
3. se ponen en cola 5 ítems por segundo en la cola global (debido a la ráfaga).
4. cada uno de esos elementos pone en cola un elemento más en la cola local y espera.
5. cuando se crea un nuevo hilo (debido a la inanición, starvation), mira en su cola local pero está vacía, ya que se
acaba de crear la cola global, que crece constantemente.
6. el encolado a la cola global (5/seg) es más rápido que el crecimiento del threadpool (0,5/seg) por lo que nunca
recuperamos las tareas en cola localmente que no se procesan ya que los hilos recientemente creados están
cogiendo tareas de la cola global. Así que los hilos de los workers se quedan bloqueados esperando...
La solución:
• En el punto 3 cambia la cola a local.
• Cuando se crea un nuevo hilo (debido al starvation), mira su cola loca, pero esta vacia por qué se acaba de crear en
la global, al no poner nada en esa cola.
• Y ahora nunca esperan de forma asincróna.
Teoría y Práctica
ThreadsPool en .Net(9/14)
Operaciones I/O
• Emitimos un I/O (he asumido que conocéis Input/Ouput, Entrada/Salida o E/S).
• Si se puede se completa inmediatamente o devuelve un código de estado indicando que la I/O esta pendiente.
• Asigna un puerto de finalización I/O, para poder observar el resultado:
− Puede representar muchas operaciones de I/O simultaneas en curso .
− Uno o varios hilos pueden esperar a IOCP (Input/Output completion port).
− Se ejecuta un callback con la información.
• En los ThreadPool de .NET:
− Uno o varios IOCP pueden estar observando cambios.
− Se marca la tarea como completada.
− Y a continuación se encola al SincronizationContext o al ThreadPool.
• Las operaciones IOCP bloquean, en realidad se bloquea el IOCP a si mismo:
− Un hilo puede manejar un gran numero de notificaciones IOCP.
IOCP Queued
Thread
Queued
Thread
Queued
Thread
Worker
Thread
Worker
Thread
Notificación de
finalización
Queued
Thread
Cola de worker inactivos preparados para ser activados
Teoría y Práctica
ThreadsPool en .Net(10/14)
Monitorización
Desde programación:
var workerThreads = 0;
var completionPortThreads = 0;
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
Con:
dotnet-counters
La documentación aquí: ir al enlace. Pero os dejo un ejemplo para hacerlo lo más simple posible con una WebAPI:
PerfomanceCounterSample.
Arranca la web api y que salga swagger y ahora entra en la línea de comandos del CLI para ver si tenemos esta
herramienta, si no, sigue estos pasos de aquí para instalarla.
Teoría y Práctica
ThreadsPool en .Net(11/14)
Teoría y Práctica
ThreadsPool en .Net(12/14)
Tambien podemos usar dotnet-trace: Donde os dejo los pasos para la instalación: ir al enlace. O este otro de
GitHub. Debéis ejecutarlo como administrador:
Teoría y Práctica
ThreadsPool en .Net(13/14)
El fichero .nettrace debes poder cargarlo en PrefView (ir aquí)
Teoría y Práctica
ThreadsPool en .Net(14/14)
Para finalizar con las herramientas más importantes: dotnet dump collect –p
Y aquí la documentación de esta herramienta de volcado.
Ejecutas: dotnet dump analyze .\dump_20220714_133404.dmp
A profundizar…
ThreadsPool en .Net (Avanzado)(1/4)
Tienes tarea –Tocaremos algunos puntos importantes muy por encima.
Estos puntos que a continuación os numero, deberá profundizarlos por tu cuenta:
• Queue Starvation (por defecto a 0).
• ThreadPool_DebugBreakOnWorkerStarvation , rompe en el depurador si el ThreadPool detecta trabajo
• ThreadPool_DisableStarvationDetection, desactiva la función de ThreadPool que obliga a los nuevos hilos
a ser añadidos cuando los workitems estan corriendo mucho tiempo (por defecto 0).
• ThreadPool_EnableWorkerTracking , habilita el seguimiento extra de cuántos hilos de trabajo simultáneos
hay (por defecto 0).
• ThreadPool_ForceMaxWorkerThreads, anula la configuración MaxThreads para el pool de trabajadores
ThreadPool (por defecto 0).
• ThreadPool_ForceMinWorkerThreads, anula la configuración de MinThreads para el pool de trabajadores
ThreadPool (por defecto 0).
• ThreadPool_UnfairSemaphoreSpinLimit, número máximo de cambios por procesador de un trabajador.
• threadpoolThreadpoolTickCountAdjustment, ajuste interno que se utilizado sólo en la compilación de
depuración del tiempo de ejecución
• HillClimbing, un grupo de ajustes para el algoritmo Hill Climbing.
• ThreadPool_EnabledWorkerTracking, es el más importante de todos ya que nos da trazas, pero es muy
costoso.
A profundizar…
ThreadsPool en .Net (Avanzado)(2/4)
ThreadPool.UnsageQueueUserWorkItem()
El execution context que incluye varios datos informativos de seguridad (impersonación, información establecida por el
thread) no se captura en las llamadas (en la Queue) por tanto, no se puede usar cuando se invoca un callback en el
thread pool. Pero es muy rápido.
ThreadPool.UnsafeQueueNativeOverLapped()
Pone en cola una operación de I/O superpuesta para su ejecución (en el pool de hilos IOCP).
ThreadPool.UnsafeRegisterWaitForSingleObject()
Version no segura de RegisterWaitForSingleObject, por lo tanto no se captura el execution context.
A profundizar…
ThreadsPool en .Net (Avanzado)(3/4)
ThreadPool Asynchronous Delegates (legacy para este curso, pero no para devs de .Net Framework)
ThreadPool.QueueUserWorkItem no proporciona una manera fácil de pasar más de un parámetro tipado y recibir el
resultado de un hilo después de que haya terminado de ejecutarse.
El delegado asíncrono resuelve esta limitación, permitiendo pasar cualquier número de argumentos tipificados en
ambas direcciones. Además, las excepciones no manejadas en los delegados asíncronos se vuelven a lanzar en el hilo
original (o más exactamente, en el hilo que comprende BeginInvoke / EndInvoke), no necesitan un handling
explícito.
En el ejemplo AsyncchronousDelegatesSample, podéis ver que no esta soportado por .NET6:
A profundizar…
ThreadsPool en .Net (Avanzado)(4/4)
Pero cambiando a .Net Framework 4.8, ya podéis probarlo:
Patrones que ya han quedado obsoletos
En este enlace de Microsoft tienes varios patrones que podrás investigar por tu cuenta: TAP, EAP y APM.
Buenas Prácticas…
ThreadsPool en .Net
Fuera de bromas, aquí van mis consejos:
Mi primer consejo es que leas toda la documentación relacionada con Parallel Programming de Microsoft.
Lo que debes leer en segundo lugar es: Procedimientos recomendados para el subproceso administrado de Microsoft.
En tercer lugar toca ver los: Procedimientos recomendados de rendimiento de ASP.NET Core.
El cuarto lugar revisar la: Biblioteca de procesamiento paralelo basado en tareas (TPL).
Y por último te recomiendo la lectura de:
Concurrency in C# Cookbook. Y entrar en el blog: https://blog.stephencleary.com/ del autor.
Todos los anteriores enlaces proporcionan una serie de buenas prácticas y recomendaciones por parte de Microsoft,
que sería volver a repetirlo aquí, por eso os mando directamente a la fuente original.
Como ves es un tema tedioso que necesita muchas horas de estudio.
1ª regla de threading es no hacer threading – Y evita dolores de cabeza
Kestrel Hosting
IIS Hosting(1/11)
.NET 6 – Documentación de Microsoft
Kestrel es un servidor web Kestrel. Es un servidor web que se incluye y habilita de forma predeterminada en las
plantillas de un proyecto .NET.
Créditos de la imagen: Microsoft. Ver documento.
Establece una asincronía de I/O basada inicialmente en la librería LIBUV (de Node.JS)
créditos de la imagen: docs.libuv.org
Kestrel Hosting
IIS Hosting(2/11)
Con la imagen anterior de LIBUV podrás ver que tiene que ver que os cuente esto de aquí: los ThreadPools y los IOCP.
En la primera versión.NET Core Microsoft uso LIBVU, pero luego añadió una capa llamada Kestrel. En ese momento
Node.JS y ASP.NET compartían el mismo servidor HTTP.
Desde la evolución de .NET y su crecimiento, se han implementado muchas mas funcionalidades, hasta el punto que
Microsoft a construido su propios servidor HTTP basado en .NET Sockets y a eliminado LIBVU que era una dependencia
que ni poseían ni la controlaban. Ahora Kestrel, es un servidor HTTP con todas las funcionalidades para ejecutar .NET.
El IIS actúa como proxy inverso que reenvía el trafico a Kestrel y gestiona el proceso Kestrel de Kestrel. En Linux,
normalmente se utiliza NGINX como proxy inverso para Kestrel.
Os voy a dar una pincelada de algún parámetro que podemos cambiar en la configuración de Kestrel.
Lo primero es anular un poco el WebHostBuilder para configurar Kestrel, solo podrás a partir de ASP.NET Core 3.0.
Es decir que el servidor web de Kestrel está configurado para el constructor de hosts.
Desde la versión ASP.NET Core 5 LIBUV esta obsoleto. No recibe actualizaciones y cualquer llamada a UseLibuv
debes cambiarla por el socker predeterminado de Kestrel.
Kestrel Hosting
IIS Hosting(3/11)
En el fichero appsettings.[environment].json:
{
"Kestrel": {
"Limits": {
"MaxConcurrentConnections": 100,
"MaxConcurrentUpgradedConnections": 100
},
"DisableStringReuse": true
}
}
O en el código:
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxConcurrentConnections = 100;
serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
});
Kestrel Hosting
IIS Hosting(4/11)
Hasta donde yo tengo entendido, cuando publicas una aplicación web en Azure una Web Apps en Windows, por tanto,
continuas usando IIS para hospedar la aplicación. Sigues teniendo ese proxy inverso para ejecutar tu aplicación en el
servidor HTTP Kestrel. Lo que os voy a contar a continuación aun tiene mucha vigencia y es muy importante que
conozcáis que ocurre con nuestra aplicación de .NET.
Si queremos que el pool de thread de IIS nos de un alto rendimiento para altas cargas de trabajo tenemos que saber
que IIS puede sufrir el anterior mencionado Thread Pool Starvation (Teoría y Práctica / ThreadPools en .Net (8/14)
) y
tambien es posible que sufra un thread pool exhaustion (agotamiento de los subprocesos).
Lo primero que debes saber que existe una diferencia entre el ThreadPool de IIS y el ThreadPool de .NET (de
aplicación). Para ello vamos a ver el ThreadPool de IIS:
IIS mantiene un ThreadPool que elimina rápidamente las solicitudes de la cola de la aplicación y las lleva a
procesar con el worker de IIS.
Ya hemos aprendido que los thread estan limitados en cuanto a crecimiento y creación. Por tanto si todos los
subproceso de IIS estan ocupados, este ThreadPool puede llegar a agotarse, por tanto puede provocar problemas
de rendimiento.
Veamos esquemáticamente como funciona IIS y como navega la información.
Kestrel Hosting
IIS Hosting(5/11)
Client
Browser/app
Incoming Request
Response
HTTP.SYS
Driver: handler TCP connection, TLS/SSL Channels, Request validations, Windows Authentication,
Kernel request listener, Kernel Cache
TCPIP.SYS
IIS
WAS Config.
IIS Worker
w3wp.exe
IIS Worker
w3wp.exe
IIS Worker Process (DefaultAppPool = Worker 1)
w3wp.exe
IIS Thread Pool
A continuación puedes ver los componentes:
• Cliente que lanza y recibe request.
• HTTPS.SYS + TCPIP.SYS donde se asienta IIS.
• IIS con los posibles workers, por defecto es 1.
• Y los procesos.
Kestrel Hosting
IIS Hosting(6/11)
IIS
WAS Config.
IIS Worker
w3wp.exe
IIS Worker
w3wp.exe
IIS Worker Process (DefaultAppPool = Worker 1)
w3wp.exe
IIS Thread Pool
Application
Request Processing
Incoming Request Response
HTTP.SYS
Driver: handler TCP connection, TLS/SSL Channels, Request validations, Windows Authentication,
Kernel request listener, Kernel Cache
TCPIP.SYS
APS.NET
Kestrel Hosting
IIS Hosting(7/11)
Cuando un proceso de trabajo se inicia por primera vez, intentará crear la cantidad mínima de subprocesos IIS en
el grupo de subprocesos de IIS. Y:
• Recibe las solicitudes de la cola al grupo de aplicaciones.
• Configura el contexto.
• Ejecuta la pipeline de la aplicación ASP.NET.
• Y va vaciando para emitir la respuesta.
Nada nuevo, el funcionamiento se extrapola con lo anteriormente conocido de colas, en resumen: existe un
ThreadPool por encima de nuestro ThreadPool de Aplicación.
Para un trabajo de una sola solicitud se puede procesar en un hilo de IIS e ignora los conmutadores asíncronos.
De nuevo mismo funcionamiento que el estudiado con las CPUs.
Pero lo habitual es que las solicitudes casi siempre se mueven a un grupo de subproceso de la aplicación.
Los hilos de aplicación se procesan en subprocesos del CLR, de ASP.NET, etc. Por tanto IIS realiza un procesado
mínimo y casi nunca se bloquean.
Kestrel Hosting
IIS Hosting(8/11)
En la mayoría de aplicaciones, no tendrá lugar en subproceso de IIS. Lo recalco:
La request empieza
en un hilo de IIS.
Se mueve al hilo de
ASP.NET tan pronto
como el módulo
ASP.NET esté
cargado.
Y la mayoría de las
veces permanece en
el hilo de APS.NET
hasta su finalización.
Kestrel Hosting
IIS Hosting(9/11)
Los subprocesos de IIS realizan poco procesamiento en comparación con la aplicación, en la mayoría de las
ocasiones.
Los subprocesos de IIS casi nunca se bloquean en las aplicaciones actúales. Debido a que las operaciones de
bloqueo de subprocesos o de ejecución prolongada ocurren dentro del código de la aplicación, bloquean el
subproceso de la aplicación en su lugar, no el subproceso de IIS. Por tanto, es común que experimentemos
bloqueos debido a la inanición (Starvation) del grupo de proceso del CLR, esto no tiene nada que ver con IIS.
Una vez contado eso, existen casos muy específicos que el ThreadPool de IIS se puede agotar, es decir, que
experimentemos que se agote un ThreadPool o encontrarnos con problemas de rendimiento que perjudican el
funcionamiento de la aplicación. Esto se hace primero: monitorizando el IIS.
Kestrel Hosting
IIS Hosting(10/11)
Tenemos casos que podemos reconocer y así iréis directo a donde buscar.
Agotamiento e Inanición del ThreadPool de IIS.
Los subprocesos de IIS son los responsables de eliminar las solicitudes de la cola del grupo de aplicaciones, es
decir, que veremos una cola de aplicaciones en constante crecimiento.
Los contadores que nos permiten observarlo son:
• HTTP Service Request Queues\Current Queue Size
• HTTP Service Request Queue\Rejection Rate Performance Rule
• IIS Worker Process Thread Count.
Es posible que tengas inanición o agotamiento si:
• Si tienes una cola de aplicación distinta de cero.
• Si El numero de thread activos en el “Active Thread Count” es mayor que cero.
Si el numero es mayor que 0 lo primero es que puede ocurrir es que existan bloqueos y si es muy grande que sea
inanición. Y si el valor es mayor que el máximo lo que tenemos es un agotamiento (caso muy poco probable).
Ahora una vez visto los síntomas toca un trabajo muy complejo en el código para localizar que hace que
indirectamente afecte a la cola.
Kestrel Hosting
IIS Hosting(11/11)
Algunas pistas pueden ser:
a) Procesos que sobrecargan la CPU.
b) Bloqueos de subprocesos debido a la inanición de la aplicación.
c) Bloqueo de subprocesos debido a archivos lentos o alta latencia de red.
Como se soluciona, alguna pista:
a) Optimizar el código, agregar más CPU (y probablemente más memoria), más nodos.
b) Los 503 suelen solucionarse con reinicios, pero esto no es la solución: usar herramientas de diagnóstico de
memoria y similares.
c) Mover archivos de una unidad de red lo más cerca del servidor IIS. Revisar la calidad de la red, reducir los
tamaños, …
Una última nota: ya sabemos como funciona el cambio de contexto para evitar los problemas de cambios de
contexto IIS lo que hace es que solo permite 1 subproceso por núcleo del procesado, esto no da RPS más altos, si
no más bajos ya que se gasta mucho tiempo en el cambio de contexto (más adelante tengo un ejemplo con
Async y Sync de una API de .NET donde se evidencia esto).
Demos – Un par de prácticos ejemplos para terminar esta parte
https://github.com/jmfloreszazo/From0to100NetAsync
Ejemplos de ThreadPool
GitHub(1/2)
Ejemplos de ThreadPool
GitHub(2/2)
ThreadPoolQueueHillClimbing
Donde lanzamos hilos para que se aprecie como funciona queue y hill-climbing.
ThreadPoolWithBenchmarks
Este ejemplos muestra la diferencia entre usar un método con Thread y otro con ThreadPool.
Teoría y Práctica
Task(1/28)
Una de las claves de cualquier aplicación empresarial es la escalabilidad, es decir, escalar para reducir el tiempo que
tardarnos en servir una petición, aumentar el número de peticiones que un servidor puede procesar, y aumentar el
número de usuarios que una aplicación puede atender simultáneamente sin aumentar el tiempo de carga.
El uso adecuado de programación asíncrona y paralelismo puede hacer que las métricas mejores y lo mejor de todo es
que C# tiene una sintaxis simplificada gracias al TPL (Task Parallel Library) o async-await, con la que podemos escribir
un código muy claro y mantenible.
En esta sección aunque parezca que vuelvo a recordar conceptos anteriormente descritos, quiero que te lo tomes como
afianzar conceptos para que:
• Quede clara la jerga que se utiliza en este tipo de desarrollo.
• Desmitificar los thread, lazy initialization o ThreadPools.
• Entender que son los semáforos y el SemaphoreSlim.
• Introducir Task y paralelismos.
Otros aspectos como async-await o colecciones concurrentes para paralelismo lo iremos viendo en siguientes
secciones.
A veces tienes que volver a gatear… – Asentar conocimientos
Teoría y Práctica
Task(2/28)
Si has leído algunos de los otros documentos, verá que me gusta dejar la jerga clara para que todo el mundo hable
sobre la misma definición. En esta ocasión no lo he puesto la principio, lo pongo aquí por qué tiene más sentido en esta
sección.
Los tecnicismo a veces son duros y por eso voy a preparar una analogía para que puedas entender las cosas con algo
real, es más yo hace años programé software para este sector del ejemplo.
Imagina que estas en la cola de un restaurante de comida rápida esperando para hacer tu pedido, y mientras estas en la
cola, respondes a un correo electrónico del trabajo o hablas por MS Teams. Después, tras pedir la comida y mientras
esperas a recogerla, hablas por teléfono con un cliente.
En el restaurante (de comida rápida), tenemos muchos mostradores donde se toman pedidos, y la comida es preparada
por los operarios mientras se hacen y toman pedidos. Es decir, se hacen pedidos en cada uno de los mostradores. Los
operarios estan trabajando paralelamente preparando comida mientras se hacen pedidos.
Como te han dado un ticket con el número de pedido, dependiendo de tiempo de preparación, un pedido puede ser
entregado antes que el tuyo al mostrador de recogidas.
Usaremos alguno de los términos del ejemplo ya que mantienen el mismo concepto y la definición es igual.
Teoría y Práctica
Task(3/28)
Concurrencia
Paralelismo
Asincronía
Task 1 Task 1
Task 1
Task 2 Task 2
context
switch
context
switch
Task 1
Task 2
Task 1 Task 1 Response
Thread
Release Thread
Task 2
Thread
Release Thread
Task 2 Response
Teoría y Práctica
Task(4/28)
Concurrencia
Implica realizar muchas tareas al mismo tiempo; en nuestro ejemplo: es cuando respondemos a un correo mientras
hacemos cola para pedir en el restaurante, o que los operarios de cocina estén con varios platos al mismo tiempo, uno
iniciándolo, otros a medias y otro finalizándolo. En aplicaciones la concurrencia implica que varios hilos comparta un
núcleo y, en función de su tiempo, ejecuten tareas y realicen cambios de contexto.
Paralelismo
Se realizan múltiples tareas de forma independiente al mismo tiempo; en nuestro ejemplo: es cuando varios pedidos se
realizan desde distintos mostradores. En aplicaciones el paralelismo sería múltiples hilos/tareas que se ejecutan al
mismo tiempo en una CPU multinúcleo. Sin embargo en una de un solo núcleo tambien se puede hacer a través de
hyper-threading, normalmente es una división lógica del núcleo en más de un núcleo, con una CPU de 4 núcleos en un
hyper-threading se dividen en 2 y por tanto tenemos 8 núcleos (cores).
El siguiente ejemplo puedes revisarlo para ver su funcionamiento: ParallelSample.
Teoría y Práctica
Task(5/28)
Asincronía
La programación asíncrona es una técnica que se basa en la ejecución de tareas de forma asíncrona en lugar de
bloquear el hilo actual mientras esperan. En el ejemplo: consiste en esperar a que llamen por tu número de ticket para
que te acerques al mostrador a recoger tu pedido mientras operarios de cocina estan trabajando en tu comida. Pero
mientras esperas, te alejas del mostrador de pedidos, permitiendo así que otras personas recojan su comida. Esta es la
forma en como se ejecuta la asincronía y como libera recursos mientras esperas a una tarea (como por ejemplo I/O,
acceso a datos de una base de datos).
Lo importante de la asincrónica es que las tareas se ejecuten paralelamente o concurrentemente gracias a un
framework y liberar a los desarrolladores de este trabajo para centrarse en la lógica de negocio.
El siguiente ejemplo: BasicAsycTest ponemos en práctica algo sencillo.
Multithreading
Es una forma de lograr concurrencia donde los nuevos hilos se crean muralmente y se ejecutan de forma concurrente,
como el ThreadPool del CLR (que ya hemos visto). En un sistema con multiprocesador/multinúcleo, el mulithreading
ayuda a conseguir paralelismo al ejecutar hilos en diferentes núcleos.
Teoría y Práctica
Task(6/28)
Threads, Process & Queues
El hilo es la unidad más pequeña de un sistemas operativo y ejecuta instrucciones en el procesador. Un proceso es un
contenedor de ejecución más grande y el hilo dentro del proceso es la unidad más pequeña de tiempo del procesador
que ejecuta instrucciones. La clave para recordar es que siempre que tu código necesite ser ejecutado en un proceso,
debe ser asignado a un hilo.
Cada procesador sólo puede ejecutar una instrucción a la vez; es por qué, es un sistema de un solo núcleo, en un
momento dado, sólo se esta ejecutado un hilo.
Existe algoritmos que gestionan el tiempo de procesado del hilo.
Un hilo puede tener una pila (que lleva la cuenta del historia del ejecuciones), registros que almacenan variables,
contadores, etc. para ser ejecutadas.
Una aplicación típica de .Net Core tiene un solo hilo cuando se inicia y puede añadir creándolos manualmente, ya
hemos visto como hacerlo, aunque lo recordaremos tambien aquí, ya que es necesario que la base este muy afianzada
antes de llegar a la sección 3.
Teoría y Práctica
Task(7/28)
Threads
Podemos crear nuevos hilos creando instancias de System.Threading.Thread y pasando un método delegado.
Por ejemplo:
Thread loadFileFromDisk = new Thread(LoadFileFromDisk);
void LoadFileFromDisk(object? obj)
{
Thread.Sleep(1000);
Console.WriteLine(“File loaded from file");
}
loadFileFromDisk.Start();
Thread fetchDataFromAPI = new Thread(FetchDataFromAPI);
void FetchDataFromAPI(object? obj)
{
Thread.Sleep(1000);
Console.WriteLine(“Data requested from API");
}
fetchDataFromAPI.Start();
Console.ReadLine();
Podemos ver que los métodos LoadFileFromDisk y FetchDataFromAPI se ejecutan cada uno en un nuevo hilo.
Recordar en el cambio de contexto y las CPU/Nucleos, esto tiene un coste, no siempre es mejor los hilos.
Teoría y Práctica
Task(8/28)
Aunque la gestión de hilos tiene sus ventajas para tener control sobre como se ejecutan, tambien tiene sus
inconvenientes:
• La gestión del ciclo de vida de los hilos, como la creación, reciclaje y cambio de contexto.
• Implementación de conceptos como tracking/reporting para la ejecución de hilos. Además, de la cancelación que es
compleja y es limitada.
• El manejo de excepciones, que si no se hace adecuadamente, puedes bloquear la aplicación.
• La depuración, las pruebas y el mantenimiento del código puede ser a veces complejo y, a veces, pueden provocar
problemas de rendimiento si no se gestionan correctamente.
Y aquí es donde entra en juego los ThreadPools del CLR (Common Language Runtime) que vamos a ver ahora.
ThreadPool
Los hilos pueden ser creados haciendo uso de los pools de hilos gestionados .Net, conocido como CLR ThreadPool. El
ThreadPool de CLR es un conjunto de hilos de trabajo que se cargan en la aplicación junto el CLR y se encargan del
ciclo de vida de los hilos, incluyendo el reciclado de hilos, la creación y el cambio de contexto. El ThreadPool del CLR
puede ser consumidos por varias API de System.Threading.ThreadPool. En concreto, para programar una
operación en un hilo, existe el método (que ya hemos visto) QueueUserWorkItem, toma un delegado del método que
necesita se programado. Continuando con el ejemplo anterior, podríamos poner:
ThreadPool.QueueUserWorkItem(FetchDataFromAPI);
Teoría y Práctica
Task(9/28)
Tal y como indica su nombre, QueueUserThreadPool, lo que hace es usar colas, por tanto cualquier código que se
ejecute se pondrá en una cola y luego se retirará de ella, es decir, se asigna un work thread con FIFO. ThreadPool esta
diseñado con una cola global.
Cuando se crea un nuevo hilo en el ThreadPool, éste mantiene su propia cola local que comprueba la cola global y retira
el elemento de trabajo de una manera FIFO; sin embargo, si el código se ejecuta en ese hilo crea otro hilo, es decir, éste
se crea en la cola local y no en la cola global.
El orden de ejecución de las operaciones de la cola local del worker thread es siempre LIFO, y la razón de esto es que el
elemento de trabajo creado más ranciamente puede estar aun caliente en caché y por lo tanto puede ser ejecutado
recientemente. Además, podemos decir que en cualquier momento, habrá n+1 colas en el ThreadPool, donde n es el
número de hilos de ThreadPools, es decir, n colas locales y 1 se refiere a la cola global.
Una representación de alto nivel de ThreadPool:
Cola
Local
Cola
Local
Cola
Local
Cola
Global
Worker Thread Worker Thread Worker Thread
Teoría y Práctica
Task(10/28)
A parte de QueueUserWorkItem, hay muchas otras propiedades/métodos disponibles, como ha os he mencionado,
pero recapitulemos estas que son las más importantes:
• SetMinThreads: se utiliza para establecer el mínimo de hilos de trabajo y de I/O asíncronos que tendrá ThreadPool
cuando se inicie el programa.
• SetMaxThreads: se utiliza para establecer el máximo de hilos de trabajo y de I/O asíncronos que tendrá
ThreadPool, después del cual, las nuevas peticiones se ponen en cola.
Por ejemplo usar QueueUserWorkItem, tiene limitaciones, que servirán para que veas siempre los pros y contras
cuando estudies la librería:
• No podemos obtener una respuesta de un worker programado en el ThreadPool, por eso el delegado es void.
• No es fácil seguir el progreso del trabajo de un hilo programado en el ThreadPool.
• No está pensado para larga duración.
• Los ThreadPools son de background, y la diferencia que existe con los de primer plano, es que si se cierra el de
primer plano, no espera a que se terminen los de background.
Debes ser consciente de la limitaciones de cada elemento del API de System.Threading.ThreadPool, por tanto lo
mejor es usar la librería TPL, que es la opción con el enfoque más sencilla y no tiene las limitaciones anteriores que nos
permitirá que nuestra aplicación sea escalable. Esta librería la veremos más adelante.
Teoría y Práctica
Task(11/28)
Lazy Initialization
La traducción es inicialización perezosa, este patrón de creación de objectos hace que se aplace la creación hasta que
sea usada por primera vez. Se basa en la premisa de que mientras la propiedades de una clase no se utilicen, no existe
ninguna ventaja en inicializar el objeto. Por tanto, esto retrasa la creación de un objeto y, en última instancia, reduce la
hulla de memoria de la aplicación, mejorando el rendimiento. Un ejemplo, que ya habrás usando cientos de veces, es el
objeto de la conexión de una base de datos.
Este patrón es buena opción para clases que contienen muchos datos y cuya creación es potencialmente costosa. Por
ejemplo, una clase que carga todos los productos de una plataforma de comercio electrónico puede ser inicializado
sólo cuando sea necesario listar los productos.
Una implementación típica de una clase de este tipo, restringe la inicialización de propiedades en los constructores y
tiene uno o más métodos que rellenan las propiedades de la clase:
Teoría y Práctica
Task(12/28)
public class ProductsFile
{
string fileName;
object loadProducts;
public ProductsFile(string fileName)
{
this.fileName = fileName;
}
public object GetFile()
{
if (loadFile == null)
{
loadFile = File.ReadAllJson(fileName);
}
return loadProducts;
}
}
Es una clase que carga un fichero JSON de todos nuestros catalogo de productos (imaginaros que es para una
importación a otro sistema), no tiene sentido cargar todo el listado hasta que alguien diga que debe hacer un GetFile.
Este patrón solo carga información cuando es necesaria.
No es nuevo, es un patrón típico que se usa para caché, llamado cache-aside, en el que cargamos un objeto en caché
cuando se accede la primera vez.
Pero en hilos tenemos un reto: si llamo muchas veces a GetFile puedo bloquear por tanto necesitamos sincronizar
Teoría y Práctica
Task(13/28)
Y aquí es cuando .Net nos deja su System.Lazy para gestionar esta implementación, cuya principal ventaja es que es
segura con los hilos, aun así, si quieres programarte la tuya la tarea es cuantiosa.
Esta clase nos proporciona constructores para implementar este patrón. Y aquí os presento la forma más comunes de
usar, tambien puedes usarlo via propiedades:
public class ProductsFile
{
string fileName;
public object LoadProducts { get; set;};
public ProductsFile(string fileName)
{
this.fileName = fileName;
this.ProductsFile = $"YOUR_CODE";
}
}
Lazy< ProductsFile > productsFile = new
Lazy< ProductsFile >(() => new ProductsFile(“test.json"));
var json = ProductsFile.Value.LoadProducts;
En general este patron tambien se usa para singleton, pero eso ya es otra historia. En resumen quería que vierais que
usamos cosas sin saberlo, aquí toda la información.
Teoría y Práctica
Task(14/28)
Bloqueos, semáforos y SemaphoreSlim
Cuando hacemos paralelismo necesitamos tener mucho cuidado con las variables compartidas. Si tenemos el
siguiente ejemplo: dos usuarios intentan comprar un producto y solo queda un artículo disponible, ambos añaden el
artículo a la cesta y el primero usuario hace el pedido, mientras se procesa en la pasarela de pagos el segundo intenta
lanzar su pedido. En este caso el segundo no debería poder comprarlo si estuviera sincronizado. Además si falla el
pago, el segundo debería poder hacer la compra (si no guardamos un tiempo el producto). Por tanto, lo que estoy
intentado explicar es que la cantidad debe ser bloqueada mientras se procesa el primer pedido y ser liberado cuando
tenga un ok o un KO.
Tendremos la sección crítica lugar donde se leen y escriben las variables usadas por múltiples hilos, es decir, variables
globales que se usan en toda la aplicación y que se modifican en lugares diferentes en momentos diferentes o al mismo
tiempo. En multihilo, un solo thread podrá entrar y modificar esos valores de esta sección crítica.
Si en nuestra aplicación no estemos este tipo de variables (sección) podremos considerarla como thread-safe. Por
tanto siempre es aconsejable no usarlas. Pero como seguro que en algún momento lo necesitas, existe una sección
llamada non-thread-safe conocidas como primitivas de sincronización o construcciones de sincronización que son:
• Construcciones de bloqueo: permiten a un hilo entrar en la sección crítica para proteger el acceso a recursos
compartidos, y que lo demás hilos esperen hasta que sea liberado el bloqueo del que la bloqueo.
• Construcciones de señalización: permiten que un hilo entre en la sección crítica señalando la disponibilidad de
recursos, un productor bloquea y el consumidor espero la señal para hacer su trabajo.
Teoría y Práctica
Task(15/28)
Un block (bloqueo) es una clase básica que permite lograr la sincronización en código multithread donde cualquier
variable del bloque de bloqueo puede ser accedida por un solo hilo. En los bloqueos, el que adquiere el bloqueo tendrá
que liberarlo, y hasta entonces, cualquier otro hilo que intente entrar en el bloqueo pasa a un estado de espera.
object locker = new object();
lock (locker)
{
//Your code
}
El hilo que sea el primero en ejecutar este código adquirirá el bloqueo y lo liberará tras la finalización del bloque de
código. Los bloqueos tambien pueden adquirirse usando Monitor.Enter y Monitor.Exit, es más, internamente
C# los locks los transforma a monitor.
Unos detalles que tienes que tener en cuenta sobre locks:
• Debe ser usados en el tipo de referencia debido a su afinidad con el hilo.
• Son muy costosos en términos de rendimiento, ya que pausan los hilos y añaden retraso.
• Una buena práctica que te evitará problemas, es usar un double-check cuando adquieras un bloqueo, lo habitual es
una implementación similar a singleton.
Teoría y Práctica
Task(16/28)
Los bloqueos no estan exentos de problemas:
• Es necesario bloquear los datos/objetos compartidos dondequiera que se estén modificando. Es muy fácil obviar las
secciones críticas de la aplicación, ya que la sección crítica es más bien un término lógico. Los compiladores no lo
marcan si no hay bloqueos alrededor de un sección crítica.
• Si no se manejan correctamente, se puede llegar a un punto muerto.
• La escalabilidad es un problema, ya que sólo un hilo puede acceder a un bloqueo a la vez, mientras que los demás
hilos deben esperar.
Nota:
Existe un concepto importante llamado atomicidad. Una operación es atómica si y solo si no hay ninguna forma de leer o escribir el estado intermedio
de una variable. Es decir, si el valor de un entero se modifica y pasa de 2 a 4, cualquier hilo que lea ese valor entero verá 2 o 4; ninguno de los hilos
verá el estado intermedio del hilo en el que el entero se actualiza parcialmente, es decir pasa de 2 a 3, por ejemplo. Garantizar la atomicidad es causa
directa de hilos seguros. Utiliza colecciones concurrentes, que veremos más adelante, en lugar de bloqueos, ya que las colecciones manejan
internamente el bloqueo de las secciones críticas.
Teoría y Práctica
Task(17/28)
Los Semáforos y SemaphoreSlim (alternativa ligera a semáforos):
Son blóquenos no exclusivos que soportan sincronización que permite que varios hilos puedan entrar en la sección
crítica. Sin embargo, la diferencia con bloqueos exclusivos, es que un semáforo se usa en situaciones en la que es
necesario restringir el acceso a un conjunto de recursos, por ejemplo, un grupo de conexión de base de datos que
permite un número fijo de conexiones.
En nuestro ejemplo si existen 2 artículos solo 2 usuarios o 1 usuario podrán añadir esos 2 artículos y si un tercer
usuario quiere añadir un artículo quedará a la espera de liberar 1 por parte de los otros compradores.
Tenemos un API en System.Threading.Semaphore que nos permite:
• Pasar el numero inicial de peticiones activas.
• El número total de peticiones concurrentes permitidas.
Por ejemplo:
var s = new Semaphore(0, 10);
0 significa que ninguna petición ha adquirido el recurso compartido y como máximo se permiten 10 peticiones
concurrentes.
Teoría y Práctica
Task(18/28)
Para adquirir necesitamos llamar a WaitOne() y para liberar a Release().
La opción ligera, suele basarse en un concepto llamado spinning. Cada vez que un recurso necesita bloquear un recurso
compartido, en lugar del bloquear el recurso inmediatamente , SemaphoreSlim utiliza un pequeño bucle que se ejecuta
durante unos microsegundos para no tener que pasar por el proceso costoso de bloqueo, cambio de contexto y
transición al kernel (los semáforos usan el kernel de Windows para bloquear un recurso). Finalmente, SemaphoreSlim
vuelve a bloquear si el recurso compartido todavía necesita ser bloqueado.
Es casi igual que el anterior semáforo:
var s = new SemaphoreSlim(0, 10);
Cosas a tener en cuenta:
• Un semáforo se utiliza para acceder a un conjunto de recursos, los semáforos no tienen afinidad con los hilos, y
cualquier hilo puede liberar un recurso.
• Se pueden usar Semaphore con nombre, que se pueden utilizar para bloquear recursos entre procesos; sin embargo
SemaphoreSlim no puede.
• La diferencia enter una y otra es que SemaphoreSlim es soporta métodos asíncronos y cancelación, por tanto se
puede usar con async-await (que veremos en la sección 3).
Teoría y Práctica
Task(19/28)
Constructor de Sincronización Tipo Uso Ejemplo
Lock/Monitor Bloqueo Para bloquear sección crítica por un solo hilo. Cualquier variable de la aplicación.
Mutex Bloqueo Para bloquear sección crítica por un solo hilo en el
proceso o entre procesos.
Solo la instancia de un proceso
necesita permiso, entre varios mutex
de la aplicación.
Semaphore
SemaphoreSlim
Bloqueo Para bloquear el pool de recurso en una aplicación a
través de un proceso o entre procesos
Pool de una conexión de una base de
datos.
AutoResetEvent Señalización Permite que un recurso sea accedido por un hilo a través
de una señal. Se suele usar para recursos que son
accesibles bajo una condición en un hilo.
https://docs.microsoft.com/es-
es/dotnet/api/system.threading.autore
setevent?view=net-6.0
ManualResetEvent
ManualResetEvenSlim
Señalización Permite desbloquear todos los hilos en espera mediante
esta señal.
https://docs.microsoft.com/es-
es/dotnet/api/system.threading.manu
alresetevent?view=net-6.0
Volatile Bloqueo Bloqueo que garantiza que usa la memoria para escribir y
modificar. Es muy útil para multi-cores.
https://docs.microsoft.com/en-
us/dotnet/api/system.threading.volatil
e?view=net-6.0
Teoría y Práctica
Task(20/28)
“Task, representa una sola operación
que no devuelve un valor
y que normalmente se ejecuta de forma asíncrona”
Microsoft, Task Clase
ahora toca andar…
Teoría y Práctica
Task(21/28)
La programación asíncrona ayuda a que nuestras aplicaciones escalen y respondan mejor, por lo que implementar aplicaciones
de este tipo no debería ser un sobresfuerzo. Aunque Thread y ThreadPool ayudan, añaden mucho sobresfuerzo y
limitaciones.
Por eso Microsoft a creado herramienta que nos ayudan en este tipo de desarrollo. Y aquí es donde profundizamos en
modos de programación Task y TPL.
La idea de la programación asíncrona es que ninguno de los hilos debe estar esperando en una operación, el framework
debe tener la capacidad de envolver una operación en alguna abstracción y luego reanudarla una vez que la operación
se ha completado sin bloquear ningún hilo. Esa abstracción es Task, que se expone vía Sytem.Threading.Task y
nos ayuda con codigo asíncrono en .Net.
Task simplifica mucho cualquier operación, ya sean recuperar datos de una base de datos, lectura de un archivo o
cualquier tarea intensiva de CPU, y simplifica la ejecución en un hilo separado si es necesario. Estas son las
características más importantes:
• Task admite genéricos Task.
• Task se encarga de programar los hilos en los ThreadPools, particionar aplicaciones y programar más de un hilo en
un ThreadPool, abstrayendo complejidad.
• Soporta CancellationsToken via IProgress.
Task & TPL – Ahora sí, ya hemos llegado a las Task tras este resumen de conocimientos
Teoría y Práctica
Task(22/28)
• Las excepciones se propagan en la aplicación que llama, incluso en jerarquías multihilo (padre/hijo).
• Y las más importante es que Task soporta async-await, que ayuda a reanudar el procesamiento de la
aplicación/método llamador una vez que la tarea se ejecuta.
TPL es un grupo de APIs de System.Threading.Task y System.Threading, que proporciona formas de crear y
gestionar tareas. Las tareas se puede crear cuando llamamos a System.Threading.Task y pasando un bloque de
código que debe ser ejecutado en la tarea. Formas de crear tareas:
var t = new Task(() => FetchDataFromAPI(https://test.com/test));
t.Start();
Ó
var t = new Task.Run(() => FetchDataFromAPI(https://test.com/test));
Ó
var t = new Task.Factory.StartNet(() => FetchDataFromAPI(https://test.com/test));
Los métodos que tenemos de obtener la información de una API mediante FetchDataFromAPI se ejecutan en un hilo
de ThreadPool y res referenciado mediante el objeto t, que es devuelto a quien llama para continuar con la operaciones.
Como esta tarea se ejecuta de forma asíncrona en el ThreadPool y el ThradPool son de background, la aplicación no
esperará a FetchDataFromAPI. El TPL exponer un método Wait() que espera a finalizar la tarea, como un
t.Wait().
Teoría y Práctica
Task(23/28)
var t = new Task.Factory.StartNet(() => FetchDataFromAPI(https://test.com/test));
t.wait();
void FetchDataFromAPI(string apiURL) {
Thread.Sleep(2000);
Console.WriteLine(“Returned API data");
}
Tambien podemos usar delegados:
var t = new Task.Factory.StartNet(delegate {FetchDataFromAPI(https://test.com/test);});
Con genéricos:
var t = new Task.Factory.StartNet(() => FetchDataFromAPI(https://test.com/test));
t.wait();
Console.WriteLine(t.result);
Esto métodos aceptan, algunso de ellos, parámetros opcionales muy importantes:
• Cancelación con CancellationToken.
• Controlar su comportamiento con TaskCreationOptions.
• Colas de tareas con TaskScheduler.
Teoría y Práctica
Task(24/28)
var t = new Task.Factory.StartNet(() => FetchDataFromAPI("YOUR_API"), TaskCreationOptions.LongRunning);
Aunque esto no garantiza una salida rápida, si no más bien da una pista al desarrollador par aunque lo optimice. Por
ejemplo, podemos partir esa tarea en cosas más pequeñas.
Tas admite la espera de múltiples tareas al mismo tiempo creando y pasando todas la tareas como parámetrso a los
siguientes métodos:
• WaitAll, espera a finalizar todas la tareas y bloquea el hilo. No recomendado.
• WhenAll, espera a que se completen las tareas sin bloquear. Normalmente usado con async-await. Recomendado.
• WaitAny, espera la finalización de una de las tareas y bloquea el hilo hasta ese momento. No recomendado.
• WhenAny, espera a que finalice una tarea sin bloquear hilo. Usando normalmente con async-await. Recomendado.
Las tareas a diferencia de los hilos, tienen un amplo soporte y gestión de excepciones. Veámoslo.
El manejo de excepciones en la tareas es tan simple como escribir un bloque Try envolviendo la tarea y luego las
excepciones, que normalmente se envuelven en AggregateException.
Try
{
var t = new Task.Factory.StartNet(() => FetchDataFromAPI("YOUR_API"));
t.wait();
}
catch (AggregateException ex)
{
console.WriteLine(ex.InnerException.Message);
}
Teoría y Práctica
Task(25/28)
En el anterior ejemplo esperamos a una sola tarea, sin embargo cuando tengamos más de una, será una colección de
InnerException. Además como tiene un Handle, que podemos suscribiré al catch y el callback nos dará la
información de la excepción. Aquí tienes un ejemplo más completo.
Las cancelaciones de las tareas, son de dos tipos:
• CancellationTokenSource, clase que crea el token de cancelación y admite cancelación vía Cancel.
• CancellationToken, estructura que escucha una cancelación y se activa si una tarea se cancela.
cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task dataFromAPI = Task.Factory.StartNew(()=> FetchDataFromAPI(new List
{
"https://test.test/test",
}), token);
cts.Cancel();
En ASP.NET que tenemos tareas asíncronas en GetAsync y PostAsync, podemos usarlo, ver este ejemplo.
Cuando realizamos aplicaciones, seguro que tendrás muchas tareas, por tanto se construye una jerarquía de tareas y
por tanto una dependencia. Esto funciona muy parecido a las promesas de JavaScript y permite encadenar tareas. Por
ejemplo:
Teoría y Práctica
Task(26/28)
Task.Factory.StartNew(() => Task1(1))
.ContinueWith(a => Task2(a.Result))
.ContinueWith(b => Task3(b.Result))
.ContinueWith(c => Console.WriteLine(c.Result));
Console.ReadLine();
En ASP.NET que tenemos tareas asíncronas en GetAsync y PostAsync, podemos usarlo, ver este ejemplo.
Aquí puedes ver como funciona ContinueWith y demás métodos relacionados.
Otro aspecto que hemos hablado mucho es del contexto de sincronización: SynchronizationContext y que volvemos
a ver brevemente.
Es una clase abstracta disponible en System.Threading que ayuda en la comunicación entre hilos. Por ejemplo, la
actualización de un elemento de la interfaz de usuario desde una tarea paralela requiere que el hilo se reincorpore al
hilo de la UI y reanude la ejecución. SynchronizationContext proporciona esta abstracción principalmente a través
del método Post de esta clase, que acepta un delegado para ejecutar en una etapa posterior. En el ejemplo anterior, si
necesito actualizar un elemento de la UI, necesito coger SynchronizationContext del hilo UI, llamar a su método
Post, y pasar los datos necesarios para actualizar el elemento de la interfaz de usuario. Como
SynchronizationContext es una clase abstracta, hay varios tipos derivados de ella.
Teoría y Práctica
Task(27/28)
Los TaskScheduler, que he mencionado antes. Cuando creamos una tarea programada en un hilo de ThreadPool, ¿quién
lo hace?.
Pues System.Threading.Task.TaskScheduler que esta disponible en el TPL para poner en cola y ejecutar
delegados de tareas en el ThreadPool. Expone una por defecto que es ThreadPoolTaskScheduler; para GUI suele
utilizar SynchronizationContextScheduler para que las tareas puedan volver y actualizar los elementos de la
interfaz de usuario. TaskScheduler y SynchronizationContext. Juegan un papel importante en async-await, y
ayudan a depurar rápidamente cualquier problema relacionado con un punto muerto.
Al final ya nos queda solamente Paralelismo para terminar esta sección. Que consiste en dividir una colección en
múltiples tareas que se ejecutan en paralelo. Con TPL tenemos paralelismo en For y ForEach gracias a la sobrecargas
que tienen.
var numbers = Enumerable.Range(1, 100).ToList();
Parallel.For(numbers.First(), numbers.Last(), x => DoSomeWork(x));
Parallel.ForEach(numbers, x => DoSomeWork(x));
Alguna de las ventajas de usar paralelismo como los anteriores son:
• Se pueden cancelar los bucles ParallelStateOptions.Break.
• Se pueden parar gracias a ParallelStateOptions.Stop.
Esto nos da mucho juego con los datos, que veremos mucho más adelante.
Teoría y Práctica
Task(28/28)
Los TaskScheduler, que he mencionado antes. Cuando creamos una tarea programada en un hilo de ThreadPool, ¿quién
lo hace?.
Pues System.Threading.Task.TaskScheduler que esta disponible en el TPL para poner en cola y ejecutar
delegados de tareas en el ThreadPool. Expone una por defecto que es ThreadPoolTaskScheduler; para GUI suele
utilizar SynchronizationContextScheduler para que las tareas puedan volver y actualizar los elementos de la
interfaz de usuario. TaskScheduler y SynchronizationContext. Juegan un papel importante en async-await, y
ayudan a depurar rápidamente cualquier problema relacionado con un punto muerto.
Al final ya nos queda solamente Paralelismo para terminar esta sección. Que consiste en dividir una colección en
múltiples tareas que se ejecutan en paralelo. Con TPL tenemos paralelismo en For y ForEach gracias a la sobrecargas
que tienen.
var numbers = Enumerable.Range(1, 100).ToList();
Parallel.For(numbers.First(), numbers.Last(), x => DoSomeWork(x));
Parallel.ForEach(numbers, x => DoSomeWork(x));
Alguna de las ventajas de usar paralelismo como los anteriores son:
• Se pueden cancelar los bucles ParallelStateOptions.Break.
• Se pueden parar gracias a ParallelStateOptions.Stop.
Esto nos da mucho juego con los datos, que veremos mucho más adelante. Además de PLINQ.
Más adelante
En más detalle…
Mucha de las cosas que hemos visto de soslayo, más adelante las
veremos con algo más de detalle y ejemplos que ayuden a su
comprensión.
Sección 3
Async
Fundamentos
async-await(1/16)
public int SomeMethod()
{
return 0;
}
public async Task SomeMethodAsync()
{
return 0;
}
public async Task SomeMethodAsync()
{
return await OtherMethodAsync();
}
Y ¿qué hemos visto? Pues como crear métodos asíncronos. El primer método es síncrono, el segundo NO es asíncrono
aunque pongamos la sentencia async, ya que realmente la asincronía la da el await. Por tanto el tercer método sí es
asíncrono. Ya sabéis identificar código asíncrono:
public async Task SomeMethodAsync()
{
await OtherMethodAsync();
OtherMethod();
}
Ejemplo – Con un pequeño ejemplo nos sumergimos
Fundamentos
async-await(2/16)
Diagrama – ¿Cómo funciona async-await?
Vamos a ver paso a paso como se ejecuta el código en este ejemplo:
async task DoProcessAsync()
{
…
int x = await DoOtherProcessAsync();
…
}
async task DoOtherProcessAsync()
{
…
int y = await stream.ReadAsync(buff);
…
return y;
}
Cola
del ThreadPool
1
2
3 4
5
6
7
8
Fundamentos
async-await(3/16)
Que ocurre en el diagrama anterior:
1. Se ejecuta DoProcessAsync, y las instrucciones correspondientes hasta llegar a una función que vea con await.
2. Se encola en el ThreadPool DoOtherProcessAsync para ejecutarla.
3. La función principal DoProcessAsync se encola en el ThreadPool
4. Se desencola DoOtherProcessAsync para que se pueda ejecutar la función.
5. Se va ejecutando la parte síncrona de la función hasta que:
• Si ve una await como en nuestro caso, ocurre lo siguiente: que coloca el proceso en el ThreadPool, salta a la
función que quieres llamar con await y volvemos a empezar.
• Si no lo hubiera la función se ejecutaría hasta el final y devolvería la información a la función que lo llama.
6. Si estamos en el caso de que dentro de la función DoOtherProcessAsync tenemos que llamar a una función
asíncrona con el await, pues es el mismo proceso que hemos visto antes.
7. En caso de haber terminado la función asíncrona del paso anterior o si todo fuera síncrono, retornamos el valor
correspondiente.
8. Continua el proceso en la función DoProcessAsync.
En todo este proceso simplificado tienes que tener en cuenta que debes aplicar el valor de quantum que habíamos visto
en anteriores explicaciones.
Y tambien tenemos que tener en cuenta que para esta gestión el procesador usa de algoritmos de máquina de estados.
En resumen estamos dejando que el ThreadPool que gestione el trabajo para logra que no se bloquee ningún proceso.
Fundamentos
async-await(4/16)
“¡Asincronía no es para rendimiento,
es para escalabilidad!”
Fundamentos
async-await(5/16)
Por ejemplo puedes hacer await varias veces sobre una tarea:
• La primera vez, si la tarea subyacente aun no se ha completado, realmente espera a la finalización de la tarea.
• En usos posteriores, como el resultado ya es conocido, estamos en un proceso síncrono.
Task task = SleepAsync(5000);
await task; // La tarea tardará x ms para ejecutarse
await task; // La tarea es inmediata ya no existe una promesa
Se puede poner un retraso, pero no siempre es útil, aun así la siguientes veces continua siendo más rápido que la
primera:
Task task = SleepAsync(5000);
await task; // La tarea tardará x ms para ejecutarse
await task; // La tarea es inmediata ya no existe una promesa
static async Task SleepAsync (int delayMS)
{
await Task.Delay(delayMs);
}
Aclaraciones – Sobre awaiting
Fundamentos
async-await(6/16)
Puede parecer lo mismo, pero no lo es:
• Una tarea continua siendo una promesa.
• Una tarea se puede esperar (await) para obtener un resultado asíncrono ( si es necesario).
async Task DoSomeAsync()
{
await DoAsync();
DoOther();
}
Conceptualmente es:
async Task DoSomeAsync()
{
Task t = DoAsync(); //Inicia la operación en background y retorna una promesa
await t; //espera a que la promesa
DoOther();
}
Como veis no es ninguna magia, si anidamos await con otro await podemos extrapolar el ejemplo anterior y verlo de
forma más conceptual.
Fundamentos
async-await(7/16)
Conviene saber bien la diferencia entre asíncrono y síncrono, estos ejemplos nos ayudarán:
public int DoSome () // Claramente es un método síncrono
{
return 0;
}
Sin embargo aunque pongamos asíncrono, realmente es síncrono, mucho cuidado con este caso, ya que estamos
usando memoria para crear una máquina de estados que nunca se usará, es un desarrollo ineficiente:
public Task DoSomeAsync ()
{
return 0;
}
Y esto sí es realmente un método asíncrono:
public Task DoSomeAsync ()
{
return await DoOtherAsync();
}
Fundamentos
async-await(8/16)
“la palabra clave async en una declaración de un
método, esta clausula no hace que el método sea
asíncrono… ¡es await quien lo convierte en asíncrono!”
Fundamentos
async-await(9/16)
¿Task o async Task?
Task CalculateAsync(int arg)
{
. . .
}
async Task CalculateAsync(int arg)
{
. . .
}
¿Cual es la diferencia?, ¿Cuál es mejor?
Depende, ambos son awaitables, pero tienen un significado diferente. A esto se le llama async eliding (asincronía por
omisión). Veamos que es.
Entremos en el siguiente ejemplo donde tenemos unos benchmark: asyncelidingtest
Más aclaraciones – Cosas que a veces creemos que sabemos y no es así
Fundamentos
async-await(10/16)
Y como se puede comprobar tras el benchmarking:
El resultado del rendimiento es llamativo como para pensárnoslo en entornos de alta disponibilidad. Pero esto no quiere
decir que debamos quitarlo. El rendimiento mejora al no crear máquinas de estado, en eso radica la ganancia.
Esta optimización implica cambios en:
• Cómo deben manejarse las excepciones.
• Y la forma en la que se liberan recursos, el dispose.
Maquina de estados:
Await en Task obtiene la finalización de la promesa, desenvolvemos Task para obtener el resultado.
Lo que está ocurriendo es que se comprueba si la operación ya está completada (para dar el resultado inmediatamente); si no, activa una máquina de
estados asíncrona que pone en marcha un MoveNext que programa una continuación en el contexto de sincronización y, si es nulo ese contexto, en
el TaskScheduler actual.
Fundamentos
async-await(9/16)
Excepciones
SleepAsyncOne(); //Case 1.A
_ = SleepAsyncOne(); //Case 2.B
await SleepAsyncOne(); //Case 2.C
SleepAsynTwo(); //Case 2.A
_ = SleepAsynTwo(); //Case 2.B
await SleepAsynTwo(); //Case 2.C
static async Task SleepAsyncOne()
{
await Task.Delay(100);
throw new NullReferenceException();
}
static async Task SleepAsynTwo()
{
await Task.Delay(100);
return Task.FromException(new NullReferenceException());
}
Vamos a explicarlo con el ejemplo anterior, para ello ve comentado las líneas una a una y ejecuta el código:
Fundamentos
async-await(10/16)
• Caso 1.A (recuerda resto comentado): No devuelve nada, se ejecuta el código y continua, el error nunca se muestra, el
programa principal termina.
• Caso 1.B (recuerda resto comentado): No devuelve nada, se ejecuta el código y continua, el error nunca se muestra, el
programa principal termina.
• Caso 1.C (recuerda resto comentado): Await hace que el código espere a que se ejecute, por tanto el error salta y se
rompe la aplicación.
• Caso 2.A (recuerda resto comentado): No devuelve nada, se ejecuta el código y continua, el error nunca se muestra, el
programa principal termina.
• Caso 2.B (recuerda resto comentado): No devuelve nada, se ejecuta el código y continua, el error nunca se muestra, el
programa principal termina.
• Caso 2.C (recuerda resto comentado): Await hace que el código espere a que se ejecute, por tanto el error salta pero
se envía con un Task.FromException que devuelve el código al hilo principal. No lo estamos rompiendo como el
1.C.
En resumen tenemos 2 tipos de situaciones en las excepciones:
- Error por el uso. Por ejemplo cuando pasamos un null o hacemos una división por cero. Un uso incorrecto de la
programación. Son errores en tiempo de compilación. Siempre se dan hasta que se arreglan.
- Error por la ejecución. Aquellos que se dan de forma intermitente, por ejemplo a veces no esta el fichero que debemos
cargar o no tiene derecho de ejecución algún tipo de usuario. Se dan en tiempo de ejecución, más complicados de
encontrar por que no se dan siempre.
Fundamentos
async-await(11/16)
Todo esto lo explico para que tengamos en cuenta como debemos gestionar los errores en la asincronía o bien se
rompe la aplicación o bien se realizan reintentos, por ejemplo.
Por tanto mi consejo es:
• Lanzar errores por uso directamente en los método de asincronía, en lugar de envolverlos y subirlos, ya que al subirlos
tenemos más posibilidades de ignorarlos. Tambien evitamos sobrecargar la máquina de estados, por tanto menos
IOPs.
• Envolver los errores por la ejecución en una Task. Esto nos permite operar sobre el y gestionarlos ya que casi siempre
debíamos esperar un resultado de esa tarea.
Por tanto:
async Task DoSomeProcessAsync() {
Task t = DoAsync(); // Para cuando queramos usar errores de uso
await t; // Para cuando queremos usar errores de ejecución
DoOtherProcess();
}
Fundamentos
async-await(12/16)
Cancelaciones
Para cancelar en operaciones asíncronas debemos usar CancellationTokens, esto nos permite cancelar
operaciones de larga duración.
No es habitual que cancelemos tareas y por tanto es interesante poder reutilizar los CancellationTokenSource, el
objeto que genera CancellationToken.
Hasta la versión 6 no había seguridad de poder hacer esto ya que no sabíamos si todavía se hacía referencia a ese
token.
En .NET 6 se amplía CancellationTokenSource con TryReset, veamos el siguiente ejemplo:
Fundamentos
async-await(13/16)
CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private void CancelAction() {
_cancellationTokenSource.Cancel();
}
public async Task DoSomeAwesomeWork() {
if (!_cancellationTokenSource.TryReset()) {
_cancellationTokenSource = new CancellationTokenSource();
}
var data = await FetchData(_cancellationTokenSource.Token);
}
public async Task FetchData(CancellationToken token) {
token.ThrowIfCancellationRequested();
var client = new HttpClient();
var response = await client.GetAsync("[YOUR_API]", token).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync(token).ConfigureAwait(false);
}
Una vez cancelado el token, no se podrá reciclar y TryReset no dará un false. En el ejemplo anterior, si hemos
realizado un CancelAction y luego hacemos nuestro DoSomeAwesomeWork, si no existe un token, instanciamos uno
nuevo.
Por tanto, mientras no se cancele el token, podremos realizar un FetchData, pero si lo hubiéramos cancelado, no
podríamos traernos nada de la API a la que llamamos.
Fundamentos
async-await(14/16)
ValueTask vs. Task
ValueTask debe utilizarse en un método asíncrono que pueda ejecutarse de forma síncrona. Lo vemos en ejemplo:
ValueTaskVsTaskTest
Este ejemplo usa una caché de memoria, si el valor no esta en la caché se ejecutará un método asíncrono en caso
contrario es proceso es todo síncrono. Aquí esta la explicación mucho más clara a la primera frase. Ahora seguro que
se entiende mejor.
Cuando ejecutemos los benchmark, vamos a ver que realmente es más rápido:
Fundamentos
async-await(15/16)
Los datos estan muy bien y son muy similares. Pero vamos a responder a ciertas preguntas que se os puede pasar por
la cabeza:
• ¿merece la pena andar revisando en una refactorización esto?, en una aplicación de baja demanda no, pero como en
cloud todo cuesta mucho dinero, aunque sea de baja demanda, es un ahorro económico, extrapola esto a un sistema
de alta demanda, no hablamos de cientos de euro, si no de cientos de euros.
• ¿a que se debe esta mejora? como veis el tiempo es muy similar, pero lo que importa es la memoria. Task se aloja en
la memoria mientras que ValueTask si no lo necesita (como nuestro ejemplo) aloja 0 bytes en memoria.
Si esto no termina de convenceros, aun tenemos más.
Task es muy flexible y tiene muchos veneficios:
• Nos permite hacer await muchas veces y por cualquier tipo de consumidor.
• Concurrencia, sin problemas.
• Puedes hacer un tipo de caché de resultados asíncronos.
• Puedes bloquear la espera hasta que se complete uno.
• Puedes usar: WhenAll, WhenAny, …
Fundamentos
async-await(16/16)
Pero… en el 100% de lo escenarios no necesitan: var result = await SomeProcessAsync()
Pregúntate si todo el proceso va a ser 100% asíncrono si no es así, tienes un claro indició de ValueTask. Creo que más
claro que el ejemplo anterior no vas a encontrar.
Y para terminar: Task aloja memoria debido a que es un tipo por referencia y eso quiere decir que el Garbage Collector
(GB) una vez completada la tarea debe hacer limpieza de recursos y objetos asignados a la tarea. A más tareas más
trabajo del GB. Por tanto, usar un tipo por valor (ValueTask) nos permitirá mejorar el rendimiento de nuestra aplicación
por ese pequeño ahorro con el GB.
Estos enlaces podrán ayudarte con algo más de información:
• Understanding the Whys, Whats, and Whens of ValueTask
• ValueTask en la documentation de Microsoft
API Controller – Un ejemplo con más contenido
ApiWithAsyncSyncBenchmark
Ejemplo
GitHub(1/3)
Ejemplo
GitHub(2/3)
A la altura del directorio src. He dejado 2 carpetas, jmeter y postman.
El test de JMeter es muy sencillo:
Como se puede observar, el throughput (request/segundo) es mayor en asincronía, no va 1 a 1 como en síncrono. Esto
nos da unas mejores tiempo Min/Max debido a que hace un mejor uso del procesador. No es que funcione más rápido.
El equipo es el mismo, los ejemplos se han realizado sobre la misma VM, en mi caso no he usado Azure Load Testing,
tendría que desplegarla en un App Service y me he ahorrado todo eso. Por tanto la frase que resalté tiempo atrás
demuestra el por qué de la asincronía… pero recordar, es await quien nos la da.
Ejemplo
GitHub(3/3)
Podemos usar Postman, y hacer un test Runner de 100, por ejemplo. Exporta a json el resultado y usa Excel para sacar
medias. O bien a simple vista:
Aunque aquí nos quedamos cortos con Excel por que no podríamos ver el throughput, que es quien hace el trabajo con
los hilos en el Kerstel (revisar secciones anteriores donde lo explico).
Avanzado
async-await(1/9)
CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private void CancelAction() {
_cancellationTokenSource.Cancel();
}
public async Task DoSomeAwesomeWork() {
if (!_cancellationTokenSource.TryReset()) {
_cancellationTokenSource = new CancellationTokenSource();
}
var data = await FetchData(_cancellationTokenSource.Token);
}
public async Task FetchData(CancellationToken token) {
token.ThrowIfCancellationRequested();
var client = new HttpClient();
var response = await client.GetAsync("[YOUR_API]", token).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync(token).ConfigureAwait(false);
}
Una vez cancelado el token, no se podrá reciclar y TryReset nos dará un false. En el ejemplo anterior, si hemos
realizado un CancelAction y luego hacemos nuestro DoSomeAwesomeWork, si no existe un token, instanciamos uno
nuevo.
Por tanto, mientras no se cancele el token, podremos realizar un FetchData, pero si lo hubiéramos cancelado, no
podríamos traernos nada de la API a la que llamamos con FetchData.
Deadlock – Literalmente es el punto muerto, pero ya veremos si la traducción es adecuada
Avanzado
async-await(2/9)
Así, con este ejemplo no queda clara la potencia que nos da usar siempre cancelaciones de token.
Supongamos un sistema de alta demanda como puede ser el buscador de Google. Habrás visto que según vas
escribiendo va lanzando consultas al buscador va haciendo llamadas al API de Google. Esto mejora la experiencia de
usuario pero si no se hace bien, saturas el servidor de peticiones que deben terminar de ejecutarse cada vez que pones
un carácter nuevo en la caja de texto.
¿Qué hace Google o qué debería hacer tu aplicación para no dejar llamadas muertas en el servidor?
Muy sencillo cancelar con el token cada llamada para que el servidor mate ese proceso y no ocupe proceso, es decir,
cada vez que escribes una letra nueva si tu petición aun esta siendo procesada en el servidor, previamente deberías
mandar una cancelación de la request anterior.
Quizá tu aplicación no lo necesite y quizás pienses que poner cancelación por todo y para todo, ya que debes
propagarlo, es un incordio; pero si estas en sistemas de alta demanda, cualquier ms o cualquier optimización de este
tipo es obligatorio.
Ahora entiendes que dejar peticiones sin cancelar, hacen que el servidor continue procesando a pesar de que ese
request no lo vas a necesitar nunca.
Avanzado
async-await(3/9)
No lo digo yo, lo dice por ejemplo este artículo, o este otro, … Podrás encontrar muchos más.
Esto viene relacionado con el contexto de la aplicación: SynchronizationContext, que usaré de pretexto para
explicar ambos términos.
En un artículo muy famoso llamado: “Async and Await” de Stephen Cleary, que es de 2012 recomienda su uso, pero es
muy peligroso por dos razones: principiantes y riesgo de mantenimiento.
Principiantes
Es un mal consejo ya que la sincronización es un tema complejo. Si le dices que use ConfigureAwait(false)
siempre a menos que lo necesite su contexto, te preguntará qué “significa necesitarlo”, por tanto, esta pregunta indica
que si no lo sabes, no debes usarlo, pero como no lo sabrán terminará por usarlo siempre…
He visto hacer un refactor indicado por un arquitecto jefe pidiendo que se ponga obligatoriamente (esto en 2019,
cuando ya se conocía la problemática), este señor introdujo un posible problema a futuro ya que es una magia negra
que hace que puedas perder horas y horas intentado ver un problema por haber introducido esto indiscriminadamente.
Existen personas con las que no se puede discutir…
Mi consejo es no usarlo a menos que sepas que es lo que hace y que el contexto no lo necesitas para nada.
ConfigureAwait(false) – Seguro que ya has leído algo al respecto
Avanzado
async-await(4/9)
Mantenimiento
Saber si necesitas o no el contexto es un tema complejo, sobre todo dependiendo a que métodos se llaman
posteriormente. Puede ser que no lo necesites ahora, pero y ¿en lo siguientes?. Es un riesgo que no debes correr al
meterlo en todas partes.
Para concluir:
Lee este artículo sobre un FAQ de ConfigureAwait.
Y yo en mi código no me preocupo donde ponerlo o, simplemente lo ignoro… aunque se que poniéndolo podría obtener
una mejora en el rendimiento, pero dudo que sea notable tanto como para justificar el riesgo.
Pero como todo en la vida, existen excepciones y esta es cuando escribas librerías que usaran otras aplicaciones, tal y
como señala el anterior FAQ, usa siempre ConfigureAwait(false):
• Una librería no debería tener contexto de aplicación y si no lo pones y alguien lo usa podrías generar un Deadlock.
• Y que debes usarlo en la librería en todos los sitios…
El artículo de Stephen esta obsoleto y en stackoverflow reconoce que se debe cambiar… por tanto no estoy siendo un
arrogante sin cabeza.
Avanzado
async-await(5/9)
Esquema de un awaiter para una Task class y SynchronizationContext
Ejecuta lo que toca en el mismo hilo sin hacer una
planificación (scheduling)
La tarea esta
completada?
Capturado por el
contexto de
sincronización?
Haces una planificación (schedule) para que
continue vía SynchronizationContext
Planificas la continuación en TaskSchedler
Si
Si
No
No
Avanzado
async-await(6/9)
Al poner las tareas en la cola para que se ejecuten, se asocian a un TaskScheduler que tiene el método QueueTask
para que sea posteriormente invocado mediante ExecuteTask.
Dispone de tres métodos importantes:
• TaskScheduler.Default, que es solamente un pool de hilos.
• TaskScheduler.Current, que es un getter proporcionado por el planificador inicial de tareas.
• TaskScheduler. FromCurrentSynchronizationContext, que crea un nuevo planificador de tareas para que
sea capturado por SynchronizationContext.Current.
Además contiene a otros dos planificadores: CurrentScheduler (que es un reader) y ExclusiveSheduler (que es un
writer). Solamente uno puede ejecutarse, es decir, no son concurrentes. Esto nos plantea varios escenarios:
• Bloqueos entre escritura/lectura.
• Limites en las concurrencias.
Y si quieres profundizar más, en este ejemplo podemos crear un limitador de subprocesos para una aplicación.
TaskScheduler – Planificador de tareas
Avanzado
async-await(7/9)
Es una factoría de tareas por completar que en algún momento te interesará llamar con TrySetResult o TrySetExcepcion
para completar la tarea. Uno de los escenarios que te interesará saber su utilidad es cuando una operación externa
asíncrona deseamos encapsulara.
Como es un tema muy complejo lo mejor es ir a la fuente que es el artículo: The Nature of TaskCompletionSource y The
Danger of TaskCompletionSource Class.
TaskCompletionSource – Útil en diversos escenarios
Avanzado
async-await(8/9)
Es un contenedor para otros contextos, como:
• Contexto de seguridad.
• Para asincronía del código, para Task.Run, ThreadPool.Queue, …
• Para Async/Await. Por ejemplo si un Await no se completa, lo captura el ExcutionContext y lo restaura cuando lo
necesite.
Y ¿qué diferencia existe entre ExecutionContext (ec) y SyncronizationContext (sc)?
• EC es un concepto más amplio que SC.
• EC se usa para las máquinas de asincronía, es muy raro que se pueda modificar y es requerido por la infraestructura
de .NET.
• SC está integrado en las tareas (Task), permite un control detallado, lo usa .ConfigureAwait() por tanto permite
personalizaciones sobre el flujo.
• EC ya no captura flujo de el SC desde la versión .NET Core 3.1, esto es bueno ya que permite optimizar el código en
el backend sin depender del EC.
ExecutionContext – Planificador de tareas
Avanzado
async-await(9/9)
Esto:
int i = await obj.SomeMethodAsync();
Es lo mismo que esto:
var awaitable = obj.SomeMethodAsync();
var awaiter = awaitable.GetAwaiter();
if (!awaiter.IsCompleted)
{
}
int i = awaiter.GetResult();
Por tanto podemos usar await a todo aquello que tenga un GetAwaiter.
Awaitables – Task y Task son awaitables
Mezclando
sync-async
Un problema habitual es que tengas que seguir un orden en una serie de llamadas a funciones y esperas que el
resultado de una venga para la otra.
Cuando estas mezclando cosas asíncronas y síncronas suele ser un gran problema, por ejemplo esperas que el orden
de una secuencia será 1, 2, 3, 4, 5 y 6, pero no espera que sea 1,3,4,2,5 y 6. Esto es un gran problema cuando manejas
este tipo de funciones.
Si comienzas a poner .GetResult o .GetAwaiter al final estarás generando bloqueos entre funciones. Muchas
veces al final se convierte todo a síncrono y te olvida de este problema, pero generas otro como es el escalado en
grandes entornos o entornos de alta demanda. Otras tantas veces lo que se hace es envolver (wrap), pero es un tema
muy peligroso que deberías descartarlo, siempre en favor a Async-Await.
Para ello lo mejor es leas este artículo de Microsoft, que nos muestra como hacer tareas secuenciales o bien ejecutar
tareas en paralelo para que al final el proceso sea como esperamos.
Un pequeño stop – Lo normal es que a veces el trabajo se solape
Otro concepto muy habitual
Fire & Forget(1/2)
Es normal encontrar código similar a esto:
BackgroundOperationAsync();
...
public async void BackgroundOperationAsync() {
var result = await CallDependencyAsync();
DoSomething(result);
}
Pero es una muy mala opción:
Ya que los métodos vacíos asíncronos bloquean el proceso si se lanza una excepción. Simplemente no puedes
captúralo.
Un método de este tipo, usa el SynchronizationContext para enviar las excepciones allí y como suele ser null, no puede
manejarlo.
Además, tampoco puedes hacer unit testing.
Ejecutar y olvidar – Ejecuto y me pongo a otra cosa
Otro concepto muy habitual
Fire & Forget(2/2)
Una posible solución es envolverlo con:
Task.Run(BackgroundOperationAsync);
...
public async Task BackgroundOperationAsync() {
var result = await CallDependencyAsync();
DoSomething(result);
}
Solo es una buena opción para retrocompatibilidad para event handles, pero no lo uses para nada más.
Si quieres ampliar más, aquí te dejo un artículo.
Avanzado (continuación)
async-await(1/)
En proceso…
En proceso… – En proceso…
Anexo
Bibliografía y algún
diagrama interesante…
Listado
Bibliografía(1/2)
Libros:
− C++ Concurrency in Action: Practical Multithreading (2nd Ed)
− Concurrency in .NET: Modern patterns of concurrent and parallel programming
− Pro .NET 4 Parallel Programming in C#
− C# 7 and .NET Core 2.0 High Performance
− Concurrency in C# Cookbook, 2nd Edition (recomiendo su lectura)
− Pro .NET Benchmarking: The Art of Performance Measurement
− High-Performance Programming in C# and .NET
Videos:
− Hands-On Parallel Programming with C# 8 and .NET Core 3.0
− Async Expert (curso que me a aportado muchos conocimientos y consolidado otros)
En parte tienen culpa… – Además te servirán como base para ampliar conocimientos
Listado
Bibliografía(2/2)
Artículos:
− https://www.leansentry.com/getstarted/website-hangs
− https://www.leansentry.com/guide/iis-aspnet-hangs/iis-thread-pool
− https://blog.stephencleary.com/2020/03/valuetask.html
Blogs:
− https://mvolo.com/
− https://blog.stephencleary.com/
Nota:
Algunas de las imágenes estan basadas en esta bibliografía así como alguno de los puntos tratados.
Si encuentra alguna similitud con alguna de las obras anteriores y no lo ves correcto, avísame
y tratamos de arreglar este libro gratuito y sin ningún tipo de lucro.
¡Gracias!
Puedes encontrarme buscando por jmfloreszazo en
https://jmfloreszazo.com