Slide 1

Slide 1 text

Experto en: Asincronía con .NET Ver. 1.0.0

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Licencia bajo… Creative Commons Atribución 4.0 Internacional ¡Esta es una Licencia de Cultura Libre!

Slide 4

Slide 4 text

Apúntate a la comunidad https://jmfloreszazo.com/azure-iot-esp/

Slide 5

Slide 5 text

Índice Resumen Sección 1 Introducción Resumen y herramientas que debes manejar Sección 2 Threading & Task Thread, ThreadPool y Task. ¿Legacy?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Requisitos previos y herramientas Resumen Conocimientos sobre codificación en C# Entorno de desarrollo Visual Studio 2022 Practicar, practicar, practicar…

Slide 8

Slide 8 text

Sección 1 Introducción

Slide 9

Slide 9 text

¿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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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:

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Ejemplo Parallel Stacks/Tasks Parallel Stacks/Tasks(2/4) Aquí podrás ver los Threads:

Slide 16

Slide 16 text

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:

Slide 17

Slide 17 text

Ejemplo Parallel Stacks/Tasks Parallel Stacks/Tasks(4/4) Y para finalizar el Parallel Watch:

Slide 18

Slide 18 text

From0to100NetAsync – Repositorio https://github.com/jmfloreszazo/From0to100NetAsync Ejemplos GitHub

Slide 19

Slide 19 text

Sección 2 Threading & Task

Slide 20

Slide 20 text

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?

Slide 21

Slide 21 text

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.

Slide 22

Slide 22 text

Teoría Threads(3/25) Cuando tenemos varias CPUs sin multicore: CPU #1 T1 CPU #2 T2

Slide 23

Slide 23 text

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.

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Teoría Threads(6/25) Veamos como funciona junto a la CPU para manejar los hilos: T1 T2 T3 T4 T5 Tn CPU Memoria

Slide 26

Slide 26 text

Teoría Threads(7/25) CPU T1 T2 T3 T5 Tn Memoria T1 contexto Registro CPU, state, TLS, … Creación del contexto T4

Slide 27

Slide 27 text

Teoría Threads(8/25) CPU T2 T3 T5 Tn Memoria T1 contexto Registro CPU, state, TLS, … T1 T4

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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.

Slide 31

Slide 31 text

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:

Slide 32

Slide 32 text

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.

Slide 33

Slide 33 text

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. − ...

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Teoría Threads(16/25) Continuando con el ejemplo anterior, extrapolemos a los hilos: CPU T1 T2 T3 T4 T5 Tn

Slide 36

Slide 36 text

Teoría Threads(17/25) CPU T1 T2 T3 T4 T5 Tn

Slide 37

Slide 37 text

Teoría Threads(18/25) CPU T1 T3 T4 T5 Prioridad Hilo Tiempo

Slide 38

Slide 38 text

Teoría Threads(19/25) CPU T1 T3 T4 T5 Prioridad Hilo Tiempo T1

Slide 39

Slide 39 text

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…

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Teoría Threads(22/25) CPU T1 T3 T4 T5 Prioridad Hilo Tiempo T1 T1

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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)

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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.

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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.

Slide 52

Slide 52 text

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.

Slide 53

Slide 53 text

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.

Slide 54

Slide 54 text

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.

Slide 55

Slide 55 text

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.

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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, …

Slide 58

Slide 58 text

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.

Slide 59

Slide 59 text

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.

Slide 60

Slide 60 text

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.

Slide 61

Slide 61 text

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, …

Slide 62

Slide 62 text

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.

Slide 63

Slide 63 text

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.

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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.

Slide 66

Slide 66 text

Teoría y Práctica ThreadsPool en .Net(11/14)

Slide 67

Slide 67 text

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:

Slide 68

Slide 68 text

Teoría y Práctica ThreadsPool en .Net(13/14) El fichero .nettrace debes poder cargarlo en PrefView (ir aquí)

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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.

Slide 71

Slide 71 text

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.

Slide 72

Slide 72 text

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:

Slide 73

Slide 73 text

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.

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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.

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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.

Slide 79

Slide 79 text

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.

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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.

Slide 82

Slide 82 text

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.

Slide 83

Slide 83 text

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.

Slide 84

Slide 84 text

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.

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Demos – Un par de prácticos ejemplos para terminar esta parte https://github.com/jmfloreszazo/From0to100NetAsync Ejemplos de ThreadPool GitHub(1/2)

Slide 87

Slide 87 text

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.

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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.

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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.

Slide 92

Slide 92 text

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.

Slide 93

Slide 93 text

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.

Slide 94

Slide 94 text

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.

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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.

Slide 98

Slide 98 text

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:

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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.

Slide 101

Slide 101 text

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.

Slide 102

Slide 102 text

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.

Slide 103

Slide 103 text

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.

Slide 104

Slide 104 text

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.

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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…

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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().

Slide 110

Slide 110 text

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.

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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:

Slide 113

Slide 113 text

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.

Slide 114

Slide 114 text

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.

Slide 115

Slide 115 text

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.

Slide 116

Slide 116 text

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.

Slide 117

Slide 117 text

Sección 3 Async

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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.

Slide 121

Slide 121 text

Fundamentos async-await(4/16) “¡Asincronía no es para rendimiento, es para escalabilidad!”

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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.

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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!”

Slide 126

Slide 126 text

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í

Slide 127

Slide 127 text

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.

Slide 128

Slide 128 text

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:

Slide 129

Slide 129 text

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.

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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:

Slide 132

Slide 132 text

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.

Slide 133

Slide 133 text

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:

Slide 134

Slide 134 text

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, …

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

API Controller – Un ejemplo con más contenido ApiWithAsyncSyncBenchmark Ejemplo GitHub(1/3)

Slide 137

Slide 137 text

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.

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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.

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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.

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

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.

Slide 151

Slide 151 text

Avanzado (continuación) async-await(1/) En proceso… En proceso… – En proceso…

Slide 152

Slide 152 text

Anexo Bibliografía y algún diagrama interesante…

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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.

Slide 155

Slide 155 text

¡Gracias! Puedes encontrarme buscando por jmfloreszazo en https://jmfloreszazo.com