Concurrencia y subprocesamiento múltiple en C#: nivel avanzado
\n\nEn el desarrollo moderno con C# (.NET), la capacidad de gestionar eficazmente la concurrencia y el subprocesamiento múltiple distingue a un ingeniero experimentado de un principiante. El conocimiento básico de Thread y los bloqueos simples ya no es suficiente para crear aplicaciones de alto rendimiento, receptivas y escalables. En este artículo, analizaremos técnicas avanzadas: desde una comprensión profunda de async/await hasta el uso de System.Threading.Channels y TPL Dataflow. El material está dirigido a desarrolladores que ya conocen los fundamentos del subprocesamiento múltiple.
1. Inmersión profunda en async/await y SynchronizationContext
\n\nMuchos desarrolladores usan async/await, pero no comprenden completamente cómo el programador de tareas (Task Scheduler) y SynchronizationContext gestionan las continuaciones. A nivel avanzado, es importante controlar en qué subproceso se ejecuta el código después de await.
Por defecto, await captura el SynchronizationContext actual (o TaskScheduler). En aplicaciones de UI (WPF, WinForms), esto devuelve la ejecución al subproceso principal. En ASP.NET Core y aplicaciones de consola, el contexto a menudo está ausente y la continuación puede ejecutarse en cualquier subproceso del grupo.
Para un ajuste fino, use ConfigureAwait(false). Esto le indica al entorno de ejecución que no intente volver al contexto original, lo que mejora el rendimiento y previene interbloqueos (deadlocks) en el código de bibliotecas.
public async Task<string> FetchDataAsync(HttpClient client)\n{\n // La continuación no capturará el contexto\n var result = await client.GetStringAsync("https://api.example.com")\n .ConfigureAwait(false);\n // Este código se ejecutará en cualquier subproceso del grupo\n return ProcessResult(result);\n}\n\npublic async Task<string> FetchDataWithContextAsync(HttpClient client)\n{\n // La continuación volverá al contexto original (por ejemplo, subproceso de UI)\n var result = await client.GetStringAsync("https://api.example.com");\n // Este código se ejecutará garantizadamente en el contexto original\n return ProcessResult(result);\n}\n\nConsejo avanzado: Cree sus propios SynchronizationContext para escenarios no estándar, por ejemplo, para organizar una cola de tareas con un número fijo de subprocesos.
2. Task Parallel Library (TPL) y paralelismo de datos
\n\nTPL proporciona abstracciones de alto nivel para el paralelismo. Parallel.For y Parallel.ForEach dividen automáticamente las colecciones en partes (chunks) y las distribuyen entre los subprocesos. Sin embargo, a nivel avanzado, es necesario comprender cómo configurar el grado de paralelismo y el manejo de excepciones.
Use ParallelOptions para limitar el número de subprocesos, así como ParallelLoopState para salir anticipadamente del bucle.
var options = new ParallelOptions\n{\n MaxDegreeOfParallelism = Environment.ProcessorCount / 2 // Usamos la mitad de los núcleos\n};\n\nParallel.ForEach(sourceData, options, (item, state) =>\n{\n try\n {\n // Operación pesada\n ProcessItem(item);\n }\n catch (Exception ex)\n {\n // Registramos y detenemos la ejecución\n Console.WriteLine($"Error: {ex.Message}");\n state.Stop(); // Detiene la ejecución del bucle\n }\n});\n\nImportante: Parallel.ForEach no garantiza el orden de procesamiento. Si el orden es importante, use Parallel.ForEach con índice o PLINQ (AsParallel().AsOrdered()).
3. Canales (System.Threading.Channels) para Productor-Consumidor
\n\nLa clase Channel<T> apareció en .NET Core 3.0 y se ha convertido en una herramienta poderosa para escenarios de productor-consumidor. Es segura para subprocesos, asíncrona y eficiente. Los canales son