Concorrência e Multithreading em C#: Nível Avançado
\n\nNo desenvolvimento moderno em C# (.NET), a capacidade de gerenciar eficientemente concorrência e multithreading diferencia um engenheiro experiente de um iniciante. O conhecimento básico sobre Thread e bloqueios simples já não é suficiente para criar aplicações de alto desempenho, responsivas e escaláveis. Neste artigo, abordaremos técnicas avançadas: desde uma compreensão profunda de async/await até o uso de System.Threading.Channels e TPL Dataflow. O material é voltado para desenvolvedores que já conhecem os fundamentos do multithreading.
1. Mergulho Profundo em async/await e SynchronizationContext
\n\nMuitos desenvolvedores usam async/await, mas não entendem completamente como o agendador de tarefas (Task Scheduler) e o SynchronizationContext gerenciam as continuações (continuations). Em um nível avançado, é importante controlar em qual thread o código será executado após o await.
Por padrão, o await captura o SynchronizationContext atual (ou TaskScheduler). Em aplicações de interface do usuário (WPF, WinForms), isso retorna a execução para a thread principal. No ASP.NET Core e em aplicações de console, o contexto geralmente está ausente, e a continuação pode ser executada em qualquer thread do pool.
Para ajuste fino, use ConfigureAwait(false). Isso informa ao runtime para não tentar retornar ao contexto original, melhorando o desempenho e prevenindo deadlocks em código de bibliotecas.
public async Task<string> FetchDataAsync(HttpClient client)\n{\n // A continuação não capturará o contexto\n var result = await client.GetStringAsync("https://api.example.com")\n .ConfigureAwait(false);\n // Este código será executado em qualquer thread do pool\n return ProcessResult(result);\n}\n\npublic async Task<string> FetchDataWithContextAsync(HttpClient client)\n{\n // A continuação retornará ao contexto original (ex.: thread da UI)\n var result = await client.GetStringAsync("https://api.example.com");\n // Este código será executado garantidamente no contexto original\n return ProcessResult(result);\n}\n\nDica Avançada: Crie seus próprios SynchronizationContext para cenários não padronizados, como organizar uma fila de tarefas com um número fixo de threads.
2. Task Parallel Library (TPL) e Paralelismo de Dados
\n\nA TPL fornece abstrações de alto nível para paralelismo. Parallel.For e Parallel.ForEach dividem automaticamente as coleções em partes (chunks) e as distribuem entre as threads. No entanto, em um nível avançado, é necessário entender como configurar o grau de paralelismo e o tratamento de exceções.
Use ParallelOptions para limitar o número de threads, bem como ParallelLoopState para sair do loop antecipadamente.
var options = new ParallelOptions\n{\n MaxDegreeOfParallelism = Environment.ProcessorCount / 2 // Usamos metade dos núcleos\n};\n\nParallel.ForEach(sourceData, options, (item, state) =>\n{\n try\n {\n // Operação pesada\n ProcessItem(item);\n }\n catch (Exception ex)\n {\n // Registramos e interrompemos a execução\n Console.WriteLine($"Erro: {ex.Message}");\n state.Stop(); // Interrompe a execução do loop\n }\n});\n\nImportante: Parallel.ForEach não garante a ordem de processamento. Se a ordem for importante, use Parallel.ForEach com índice ou PLINQ (AsParallel().AsOrdered()).
3. Canais (System.Threading.Channels) para Producer-Consumer
\n\nA classe Channel<T> surgiu no .NET Core 3.0 e se tornou uma ferramenta poderosa para cenários de produtor-consumidor. Ela é thread-safe, assíncrona e eficiente. Os canais são