Concurrencia y subprocesamiento múltiple en C#: nivel avanzado
En el desarrollo moderno con C#, la concurrencia y el subprocesamiento múltiple no son solo una ampliación de capacidades, sino herramientas fundamentales para crear aplicaciones rápidas, receptivas y escalables. A nivel avanzado, vamos más allá de simplemente ejecutar Task.Run y nos enfrentamos a los detalles de los planificadores, la sincronización de contextos y los patrones para trabajar con datos compartidos. En este artículo, analizaremos los conceptos avanzados clave de concurrencia en C# (.NET) que le ayudarán a escribir código eficiente y seguro.
1. Task y ValueTask: uso profundo
La clase Task es la base de la programación asíncrona en C#. Sin embargo, a nivel avanzado es importante entender la diferencia entre Task y ValueTask, así como gestionar el ciclo de vida de las tareas.
¿Cuándo usar ValueTask?
ValueTask es una estructura que permite evitar la asignación de memoria para un objeto Task cuando el resultado ya está disponible de forma síncrona. Esto es crítico para escenarios de alto rendimiento, por ejemplo, en métodos de almacenamiento en caché.
public class CacheService{ private readonly Dictionary<string, string> _cache = new();
public async ValueTask<string> GetDataAsync(string key) { if (_cache.TryGetValue(key, out string value)) { // El resultado ya existe, no creamos un Task return value; }
// Obtención asíncrona de datos string data = await FetchFromDatabaseAsync(key); _cache[key] = data; return data; }
private async Task<string> FetchFromDatabaseAsync(string key) { await Task.Delay(100); // Simulación de retardo return $"Data for {key}"; }}Manejo avanzado de errores y cancelación
Utilice CancellationToken para la cancelación correcta de operaciones largas. Combine varios tokens mediante CancellationTokenSource.CreateLinkedTokenSource.
public async Task ProcessWithTimeoutAsync(CancellationToken externalToken){ using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeoutCts.Token);
try { await LongRunningOperationAsync(linkedCts.Token); } catch (OperationCanceledException) { Console.WriteLine("Operación cancelada o tiempo de espera excedido."); }}2. SynchronizationContext y planificadores de tareas
Comprender SynchronizationContext es necesario para trabajar con subprocesos de UI (WPF, WinForms) y ASP.NET Core. A nivel avanzado, puede crear sus propios contextos de sincronización.
Configuración del contexto para el grupo de subprocesos
Por defecto, await captura el SynchronizationContext actual. Para evitar esto (y prevenir un deadlock), use ConfigureAwait(false).
public async Task ProcessDataAsync(){ // Este código se ejecuta en el contexto original (por ejemplo, UI) await Task.Run(() => HeavyComputation()).ConfigureAwait(false);
// Después de ConfigureAwait(false) — en el contexto del grupo de subprocesos // ¡No hay acceso a elementos de UI! Console.WriteLine("Ejecutado en un subproceso del grupo");}Creación de un planificador personalizado
Puede implementar su propio TaskScheduler, por ejemplo, para limitar la cantidad de tareas que se ejecutan simultáneamente (análogo a SemaphoreSlim a nivel de planificador).
public class LimitedConcurrencyTaskScheduler : TaskScheduler{ private readonly SemaphoreSlim _semaphore; private int _runningTasks;
public LimitedConcurrencyTaskScheduler(int maxConcurrency) { _semaphore = new SemaphoreSlim(maxConcurrency);