Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Experto en: Asincronía con .NET

Experto en: Asincronía con .NET

Primera parte del libro de asincronía para .NET, pasa de 0 a 100 en un pequeño libro ;)

Jose María Flores Zazo

December 14, 2022
Tweet

More Decks by Jose María Flores Zazo

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. Sección 1
    Introducción

    View Slide

  9. ¿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

    View Slide

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

    View Slide

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

    View Slide

  12. 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:

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 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:

    View Slide

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

    View Slide

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

    View Slide

  19. Sección 2
    Threading & Task

    View Slide

  20. 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?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. 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:

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. 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…

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  67. 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:

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  72. 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:

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  98. 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:

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  107. 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…

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  112. 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:

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  117. Sección 3
    Async

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  126. 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í

    View Slide

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

    View Slide

  128. 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:

    View Slide

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

    View Slide

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

    View Slide

  131. 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:

    View Slide

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

    View Slide

  133. 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:

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  152. Anexo
    Bibliografía y algún
    diagrama interesante…

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide