Concorrência em C#: como não deixar o programa entediado
\n\nImagine que você é um chef de cozinha. Você tem três bocas de fogão, um forno e uma batedeira. Se você fizer tudo sequencialmente — primeiro cortar a cebola, depois fritar, depois bater o creme — o almoço vai demorar uma eternidade. É muito mais eficiente ligar o forno enquanto corta os legumes e bater o creme enquanto a carne cozinha. Isso é concorrência — a capacidade de executar várias tarefas "simultaneamente", alternando entre elas para que os recursos não fiquem ociosos.
\n\nEm C#, existem três ferramentas principais para isso: threads clássicas (Thread), tarefas modernas (Task) com async/await e o conceito de goroutines, implementado através de iteradores. Neste artigo, vamos analisar cada abordagem, comparar seu desempenho e mostrar quando usar cada uma. Se você programa em C# e quer que seu código voe, em vez de rastejar, está no lugar certo.
\n\nThreads: artilharia pesada
\n\nUma thread é a maneira mais de baixo nível de organizar a concorrência. Cada thread é um "processo leve" separado dentro do seu programa, que tem sua própria pilha e contexto. Criar uma thread em C# é muito simples:
\n\nusing System;\nusing System.Threading;\n\nclass Program\n{\n static void Main()\n {\n Thread thread = new Thread(DoWork);\n thread.Start();\n Console.WriteLine("Thread principal funcionando");\n thread.Join(); // aguarda a conclusão\n }\n\n static void DoWork()\n {\n for (int i = 0; i < 5; i++)\n {\n Console.WriteLine($"Thread: {i}");\n Thread.Sleep(200);\n }\n }\n}\n\nAqui, criamos uma thread separada que conta até cinco, enquanto a thread principal exibe uma mensagem. Sem Join, o programa poderia terminar antes de chegar a cinco.
Desvantagens das threads
\n- \n
- Peso elevado. Cada thread reserva cerca de 1 MB de memória para a pilha. Criar 1000 threads significa ocupar 1 GB à toa. \n
- Complexidade de sincronização. Para que duas threads não corrompam dados compartilhados, são necessários locks, mutexes e semáforos. Um erro e você tem um deadlock ou race condition. \n
- Troca de contexto. O sistema operacional gasta tempo alternando entre threads. Se houver mais threads do que núcleos de CPU, isso se torna um gargalo. \n
As threads são boas quando você precisa de paralelismo real em um processador multi-core para cálculos pesados. Mas para operações de E/S (trabalhar com arquivos, rede, banco de dados), elas são excessivas.
\n\nTask e async/await: o padrão moderno
\n\nA partir do .NET 4.5, a Microsoft propôs uma alternativa elegante — a Task Parallel Library (TPL) e as palavras-chave async/await. Não são threads puras, mas promessas: você diz ao compilador "inicie este trabalho e, quando terminar, retorne o resultado". Enquanto o trabalho é executado, a thread não é bloqueada, mas retorna ao pool.
using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nclass Program\n{\n static async Task Main()\n {\n Console.WriteLine("Iniciando download...");\n string data = await DownloadDataAsync();\n Console.WriteLine($"Baixado: {data.Length} caracteres");\n }\n\n static async Task<string> DownloadDataAsync()\n {\n using HttpClient client = new HttpClient();\n string result = await client.GetStringAsync("https://example.com");\n return result;\n }\n}\n\nObserve: await não bloqueia a thread. Enquanto os dados são baixados pela rede, a thread retorna ao pool e pode atender outras requisições. Isso é ideal para servidores web e aplicações GUI — a interface não congela.
Como funciona por baixo dos panos?
\nQuando o compilador vê async, ele transforma o método em uma máquina de estados. Em cada await, o método "lembra" seu estado