← maurobernal.com.ar

Etiqueta: threading

  • C# 13: params modernos, Lock de primera clase y Task.WhenEach

    C# 13 con .NET 9 llegó con mejoras que parecen pequeñas pero cambian patrones que usamos hace años. Los params con Span eliminan allocations que ni sabíamos que estábamos haciendo. El Lock de primera clase hace que el threading sea más seguro. Y Task.WhenEach resuelve un patrón asíncrono que antes requería código contorsionado.

    params ReadOnlySpan<T>: cero allocations en métodos variádicos

    Antes de C# 13, params solo funcionaba con arrays. Cada vez que llamabas a un método con params int[], el compilador creaba un array en el heap aunque los datos pudieran vivir perfectamente en el stack. C# 13 cambia eso.

    // ❌ C# 12 y anteriores: params siempre crea un array en el heap
    public static int Sumar(params int[] numeros)
    {
        int total = 0;
        foreach (var n in numeros) total += n;
        return total;
    }
    
    // Cada llamada alloca un int[] en el heap:
    int r1 = Sumar(1, 2, 3);      // new int[] { 1, 2, 3 }
    int r2 = Sumar(10, 20);        // new int[] { 10, 20 }
    
    // ✅ C# 13: params con ReadOnlySpan — cero allocations
    public static int Sumar(params ReadOnlySpan<int> numeros)
    {
        int total = 0;
        foreach (var n in numeros) total += n;
        return total;
    }
    
    // El compilador puede poner los valores directamente en el stack
    int r1 = Sumar(1, 2, 3);   // Sin heap allocation
    int r2 = Sumar(10, 20);     // Sin heap allocation
    
    // También funciona con otros tipos de colección
    public static void LogTodos(params IEnumerable<string> mensajes) { ... }
    public static T Primero<T>(params ReadOnlySpan<T> items) => items[0];

    System.Threading.Lock: el tipo de primera clase para sincronización

    Durante años, el patrón estándar para sincronizar hilos en C# era lock (new object()). Funcionaba, pero tenía un problema: object no comunica la intención. Cualquier referencia podía usarse accidentalmente como lock. System.Threading.Lock resuelve eso con un tipo dedicado que el compilador trata de forma especial.

    // ❌ Antes: object genérico como lock
    public class Cache
    {
        private readonly object _lock = new object();
        private Dictionary<string, object> _datos = new();
    
        public void Agregar(string clave, object valor)
        {
            lock (_lock)
            {
                _datos[clave] = valor;
            }
        }
    
        public bool TryGet(string clave, out object? valor)
        {
            lock (_lock)
            {
                return _datos.TryGetValue(clave, out valor);
            }
        }
    }
    
    // ✅ C# 13 / .NET 9: System.Threading.Lock
    public class Cache
    {
        // Semánticamente claro: esto ES un lock, no cualquier objeto
        private readonly Lock _lock = new();
        private Dictionary<string, object> _datos = new();
    
        public void Agregar(string clave, object valor)
        {
            // El compilador emite código optimizado para Lock
            // Usa el patrón Dispose internamente (EnterScope)
            lock (_lock)
            {
                _datos[clave] = valor;
            }
        }
    
        // También disponible: API explícita para casos avanzados
        public bool TryAgregar(string clave, object valor)
        {
            using (_lock.EnterScope())
            {
                if (_datos.ContainsKey(clave)) return false;
                _datos[clave] = valor;
                return true;
            }
        }
    }

    El compilador emite una advertencia si intentás pasar un Lock como object a otro método — protección contra el antipatrón de usar el mismo lock para múltiples propósitos.

    Task.WhenEach: procesar tareas a medida que terminan

    Tenía un patrón que repetía en muchos proyectos: lanzar varias tareas en paralelo y procesar cada resultado apenas llegaba, sin esperar a que todas terminen. Antes requería Task.WhenAny en un loop con manejo manual. Task.WhenEach lo vuelve trivial.

    // ❌ Antes: patrón verbose con Task.WhenAny
    var tareas = new List<Task<Reporte>> { 
        ObtenerReporteAsync("ventas"), 
        ObtenerReporteAsync("stock"), 
        ObtenerReporteAsync("clientes") 
    };
    
    var pendientes = new HashSet<Task<Reporte>>(tareas);
    while (pendientes.Count > 0)
    {
        var completada = await Task.WhenAny(pendientes);
        pendientes.Remove(completada);
        var reporte = await completada;
        Console.WriteLine($"Reporte listo: {reporte.Nombre}");
    }
    
    // ✅ C# 13 / .NET 9: Task.WhenEach — elegante y eficiente
    var tareas = new List<Task<Reporte>> { 
        ObtenerReporteAsync("ventas"), 
        ObtenerReporteAsync("stock"), 
        ObtenerReporteAsync("clientes") 
    };
    
    await foreach (var completada in Task.WhenEach(tareas))
    {
        var reporte = await completada;
        Console.WriteLine($"Reporte listo: {reporte.Nombre}");
        // Procesa cada reporte apenas llega, sin bloquear a los demás
    }

    LINQ: CountBy, AggregateBy e Index

    var pedidos = new[] {
        new { Cliente = "Ana",  Monto = 100m },
        new { Cliente = "Juan", Monto = 200m },
        new { Cliente = "Ana",  Monto = 150m },
        new { Cliente = "Juan", Monto = 300m },
    };
    
    // CountBy: contar elementos agrupados por clave
    var pedidosPorCliente = pedidos.CountBy(p => p.Cliente);
    // { "Ana": 2, "Juan": 2 }
    
    // AggregateBy: acumular valores agrupados por clave
    var totalPorCliente = pedidos.AggregateBy(
        p => p.Cliente,
        seed: 0m,
        (acum, p) => acum + p.Monto
    );
    // { "Ana": 250, "Juan": 500 }
    
    // Index(): obtener índice sin usar Select((x, i) => ...)
    foreach (var (indice, pedido) in pedidos.Index())
    {
        Console.WriteLine($"Pedido #{indice}: {pedido.Cliente} - ${pedido.Monto}");
    }

    Nuevos miembros partial (C# 13)

    // C# 13: propiedades y constructores pueden ser partial
    // Muy útil con source generators (EF Core, Roslyn generators)
    
    public partial class Entidad
    {
        // Declaración en el archivo principal
        public partial string Nombre { get; set; }
        public partial void OnNombreCambiado();
    }
    
    public partial class Entidad
    {
        // Implementación en el archivo generado (o viceversa)
        public partial string Nombre
        {
            get => field;
            set { field = value; OnNombreCambiado(); }
        }
        
        public partial void OnNombreCambiado() 
            => Console.WriteLine($"Nombre cambió a: {Nombre}");
    }

    ← C# 12: Primary Constructors, Collection Expressions y el código que debería haber existido siempre | Serie .NET 8 → .NET 10 | Próximo: La palabra clave field en C# 14: adiós para siempre a los backing fields repetitivos →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)