VocĂȘ jĂĄ viu sua aplicação travar com OutOfMemoryException ao tentar processar um CSV de 200MB? O problema nĂŁo Ă© o C#. Ă a forma como vocĂȘ estĂĄ lendo o arquivo.
Quem trabalha com integração de sistemas, exportação de relatĂłrios ou importação de dados em massa jĂĄ passou por isso: vocĂȘ precisa processar um arquivo CSV de centenas de megabytes (ou atĂ© gigabytes) e a aplicação simplesmente trava, consome toda a RAM do servidor ou lança a famosa exceção OutOfMemoryException.
O motivo clĂĄssico para isso acontecer Ă© o uso inocente de rotinas que tentam carregar o arquivo inteiro para a memĂłria antes de processar. E eu sei bem como Ă© isso.
Eu particularmente adoro manipular arquivos com programação. Jå falei sobre isso no post Como AplicaçÔes Simples Podem Ser Poderosas, onde mostro C# manipulando arquivos TXT e scripts BAT. Não sei explicar o motivo, mas realmente adoro esse tema de ler arquivos, alterar, incluir dados. Foi o que me motivou a escrever este post.
Neste artigo, vamos entender por que o File.ReadAllLines trava e qual Ă© a abordagem correta usando C# para processar arquivos gigantescos de forma rĂĄpida, estĂĄvel e com consumo de memĂłria praticamente zero.
1. O erro comum: carregar o arquivo inteiro com File.ReadAllLines
Quando precisamos ler um arquivo texto, a primeira ideia costuma ser usar o método File.ReadAllLines():
// EVITE ISSO PARA ARQUIVOS GRANDES!
string[] linhas = File.ReadAllLines(@"C:\dados\relatorio_gigante.csv");
foreach (string linha in linhas)
{
// Processamento da linha
}
Esse cĂłdigo parece inofensivo, mas tem um problema grave: ele carrega o arquivo inteiro na memĂłria antes de vocĂȘ processar a primeira linha [Stack Overflow].
Em um arquivo de 225MB, por exemplo, o processo pode chegar a consumir mais de 1GB de RAM [Stack Overflow]. Isso acontece porque:
- O arquivo Ă© lido por completo em um
string[]; - Cada linha vira uma string na memĂłria;
- Ao mesmo tempo, o encoding e os objetos intermediårios também ocupam espaço;
- Se o arquivo tiver 500MB, vocĂȘ pode facilmente estourar 2GB de RAM.
A documentação da Microsoft Ă© clara: "Quando vocĂȘ usa ReadAllLines, vocĂȘ precisa esperar que todo o array de strings seja retornado antes de acessĂĄ-lo. Portanto, ao trabalhar com arquivos muito grandes, ReadLines pode ser mais eficiente" [Stack Overflow].
Quer dominar C# e processamento de dados de forma profissional? Conheça o curso completo:
Curso Completo de C# e .NET2. A primeira solução: File.ReadLines (lazy loading)
O jeito mais simples de resolver o problema sem trocar quase nada no cĂłdigo Ă© usar File.ReadLines() em vez de File.ReadAllLines().
A diferença crucial: File.ReadLines retorna um IEnumerable<string> que lĂȘ uma linha por vez, sob demanda [Stack Overflow].
// Use ISSO para arquivos grandes
foreach (string linha in File.ReadLines(@"C:\dados\relatorio_gigante.csv"))
{
// Processamento da linha
// A memĂłria usada aqui Ă© apenas a da linha atual
}
Com essa mudança simples, o consumo de memória cai de centenas de megabytes para poucos kilobytes [Stack Overflow].
Quando isso nĂŁo Ă© suficiente? Quando o arquivo tem campos com quebras de linha internas. Nesse caso, uma linha "lĂłgica" do CSV pode conter \r\n dentro de um campo entre aspas, e o ReadLines vai dividir errado [Stack Overflow].
3. A solução definitiva: StreamReader linha a linha
Para ter controle total sobre a leitura, encoding e buffer, o StreamReader Ă© a escolha profissional. Ele lĂȘ o arquivo em blocos (buffer) e mantĂ©m apenas o necessĂĄrio em memĂłria.
using (var reader = new StreamReader(@"C:\dados\relatorio_gigante.csv"))
{
string linha;
while ((linha = reader.ReadLine()) != null)
{
// Processamento da linha
// Apenas a linha atual estĂĄ na memĂłria
}
}
E a versĂŁo assĂncrona, para nĂŁo bloquear a thread principal:
using (var reader = new StreamReader(@"C:\dados\relatorio_gigante.csv"))
{
string linha;
while ((linha = await reader.ReadLineAsync()) != null)
{
// Processamento assĂncrono
}
}
A documentação da Microsoft destaca que o StreamReader é a forma mais eficiente de ler arquivos grandes, pois trabalha com streaming em vez de carregar tudo de uma vez [Stack Overflow].
Vantagens do StreamReader:
- Controle de encoding: vocĂȘ define UTF-8, GB2312, Latin1, etc.;
- Buffer configurĂĄvel: pode ajustar o tamanho do buffer para otimizar I/O;
- Async nativo:
ReadLineAsync()libera a thread durante a leitura; - MemĂłria mĂnima: apenas o buffer e a linha atual ocupam espaço.
4. E quando o CSV tem aspas, vĂrgulas e quebras de linha internas?
Se o seu CSV Ă© "bagunçado" â com campos entre aspas, vĂrgulas dentro de valores ou quebras de linha internas â aĂ o Split(',') nĂŁo resolve. VocĂȘ precisa de um parser de verdade.
A biblioteca mais popular do ecossistema .NET é o CsvHelper. Mas atenção: usar CsvHelper do jeito errado também causa OutOfMemoryException [php.cn].
O erro clĂĄssico Ă© chamar ReadRecords<T>(), que carrega todo o arquivo na memĂłria antes de retornar [php.cn].
// EVITE: ReadRecords carrega tudo na memĂłria
var records = csv.GetRecords<MeuModelo>().ToList();
A forma correta Ă© usar GetRecord<T>() dentro de um loop com Read() explĂcito [php.cn]:
using (var reader = new StreamReader("input.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
// LĂȘ o header primeiro
csv.Read();
csv.ReadHeader();
// Agora lĂȘ linha por linha
while (csv.Read())
{
var record = csv.GetRecord<MeuModelo>();
// Processamento
}
}
Um detalhe importante: em versĂ”es mais antigas do CsvHelper, a checagem de ShouldSkipRecord fazia o parser materializar a linha duas vezes â uma para checar e outra para vocĂȘ ler. Isso dobrava o consumo de memĂłria [GitHub].
A boa notĂcia Ă© que isso foi corrigido. Se vocĂȘ estĂĄ usando uma versĂŁo recente, o impacto Ă© mĂnimo. Se estiver preso a uma versĂŁo antiga, saiba que isso pode ser o gargalo.
Quer se aprofundar em processamento de dados com .NET? Confira este material:
E-book: Processamento de Dados em C#5. FlameCsv: a alternativa de alta performance (e baixa alocação)
Se vocĂȘ precisa de performance extrema â estamos falando de ler mĂșltiplos gigabytes por segundo â vale conhecer o FlameCsv.
Segundo a prĂłpria documentação, o FlameCsv foi construĂdo do zero com Span<T>, SIMD e MemoryPool para oferecer "near-zero allocations" [NuGet].
O que isso significa na prĂĄtica?
- Zero pressĂŁo no Garbage Collector para arquivos enormes;
- Parsing com aceleração por hardware (AVX2/SSE2);
- Suporte a UTF-8 nativo, sem conversĂŁo intermediĂĄria;
- Leitura e escrita assĂncronas.
O cĂłdigo Ă© simples:
using FlameCsv;
await foreach (var record in CsvReader.ReadAsync<MeuModelo>("dados.csv"))
{
// Processamento com alocação próxima de zero
}
Quando usar FlameCsv? Quando o volume de dados Ă© tĂŁo grande que o CsvHelper jĂĄ nĂŁo dĂĄ conta. Para a maioria dos casos do dia a dia, o CsvHelper Ă© suficiente. Mas se vocĂȘ estĂĄ processando milhĂ”es de linhas diariamente, vale testar.
6. Como medir o consumo real de memória da sua aplicação
NĂŁo adianta sĂł confiar no gerenciador de tarefas. Em C#, vocĂȘ pode medir o consumo real da sua aplicação com a classe Process [Learn Microsoft].
A propriedade PeakWorkingSet64 retorna o pico de memĂłria fĂsica (em bytes) usado pelo processo desde que ele foi iniciado [Learn Microsoft].
using System.Diagnostics;
var processo = Process.GetCurrentProcess();
long picoMemoriaBytes = processo.PeakWorkingSet64;
double picoMemoriaMB = picoMemoriaBytes / (1024.0 * 1024.0);
Console.WriteLine($"Pico de memĂłria: {picoMemoriaMB:F2} MB");
Como usar isso no seu teste:
- Meça o pico usando
File.ReadAllLinescom um CSV de 200MB; - Meça o pico usando
File.ReadLinescom o mesmo arquivo; - Meça o pico usando
StreamReader; - Compare os trĂȘs resultados.
VocĂȘ vai ver uma diferença brutal. O ReadAllLines pode consumir 500MB a 1GB, enquanto o StreamReader fica na casa dos kilobytes.
7. Resumo: qual abordagem usar em cada caso
NĂŁo existe "bala de prata", mas existe a abordagem certa para cada cenĂĄrio:
| CenĂĄrio | Abordagem | MemĂłria |
|---|---|---|
| Arquivo pequeno (< 10MB) | File.ReadAllLines |
OK |
| Arquivo médio (< 100MB) | File.ReadLines |
Baixa |
| Arquivo grande (> 100MB) | StreamReader |
MĂnima |
| CSV complexo (aspas, vĂrgulas) | CsvHelper com GetRecord |
Baixa |
| Performance extrema (GB/s) | FlameCsv | Quase zero |
A regra de ouro Ă© simples: nunca carregue um arquivo inteiro na memĂłria se vocĂȘ pode processar linha a linha. O File.ReadAllLines Ă© uma armadilha para arquivos grandes, e o StreamReader Ă© seu melhor amigo.
E aĂ, vocĂȘ jĂĄ passou por esse problema? Qual foi a solução que vocĂȘ adotou? Conta pra gente nos comentĂĄrios.
Quer dominar C# e .NET de verdade?
No Nerd Cult, a gente descomplica o desenvolvimento â da leitura de arquivos Ă arquitetura de sistemas. Assine nossa newsletter e receba conteĂșdos prĂĄticos direto no seu e-mail.
QUERO RECEBER OS GUIASBÎnus: Checklist de performance para aplicaçÔes .NET
Nerd Cult â onde o cĂłdigo encontra o rock, o cinema e a cultura geek. Porque ser nerd Ă© transformar o medo em curiosidade, e a curiosidade em poder.
#CSharp #DotNet #CSV #Performance #StreamReader #NerdCult
ComentĂĄrios (0)
Nenhum comentĂĄrio ainda. Seja o primeiro a comentar!
Adicionar ComentĂĄrio