CodeGym /Cursos /C# SELF /Leitura e escrita assíncronas de arquivos (

Leitura e escrita assíncronas de arquivos ( ReadAsync/ WriteAsync)

C# SELF
Nível 42, Lição 1
Disponível

1. Introdução

Imagine que você é o maestro da orquestra (do seu aplicativo). Se toda vez que o violinista afina o violino (um I/O demorado) você fica esperando antes de seguir para outros instrumentos, a orquestra inteira vai travar. Mas se o violinista diz: "Eu vou afinar, vocês continuem tocando, eu aviso quando estiver pronto", — isso é assincronia!

No mundo do C# e .NET 9 existem ferramentas específicas pra esse "multitasking sem travar". Os protagonistas de hoje são as versões assíncronas dos métodos Read e Write, chamadas ReadAsync e WriteAsync.

Elas permitem iniciar uma operação de leitura ou escrita e imediatamente "liberar" a thread atual pra fazer outra coisa. Quando a operação de I/O terminar (por exemplo, os dados forem lidos do disco ou escritos nele), seu código "acorda" e continua de onde parou.

Pra usar esses métodos precisamos de duas palavrinhas mágicas que chegaram no C# lá em 2012 com a versão 5.0 (e agora, no C# 14, já são velhas conhecidas!):

  • async: É um modificador que você coloca no método pra dizer ao compilador: "Dentro desse método pode ter operações assíncronas e eu vou usar await".
  • await: É o operador que você usa antes de chamar uma operação assíncrona (tipo ReadAsync ou WriteAsync). Significa: "Comece essa operação, mas não fique esperando aqui. Devolva o controle para quem chamou e volte quando a operação terminar".

Não se preocupe se isso parecer meio abstrato por enquanto. Vamos ter um nível inteiro dedicado ao async e await (Nível 58), onde vamos desmontar tudo. Agora o importante é entender que eles ajudam a não bloquear a thread principal.

2. ReadAsync: lendo com calma

O método ReadAsync permite ler dados de um stream de forma assíncrona. Em vez de esperar os bytes serem lidos do disco, você inicia a leitura e já parte pra outras tarefas.

Aqui está a assinatura principal para leitura em um buffer:

public virtual ValueTask<int> ReadAsync(
    byte[] buffer,
    int offset,
    int count,
    CancellationToken cancellationToken = default
)

Ou, mais usado no C# moderno (e .NET 9), usando Memory<byte>:

public virtual ValueTask<int> ReadAsync(
    Memory<byte> buffer,
    CancellationToken cancellationToken = default
)

Vamos analisar os parâmetros:

  • buffer: É um array de byte (ou Memory<byte>) onde os dados serão lidos. Lembra dos buffers pra otimizar? Aqui é a mesma ideia, só que pra operações assíncronas.
  • offset: Offset no buffer onde começar a escrever os bytes lidos.
  • count: Quantidade máxima de bytes a serem lidos.
  • CancellationToken cancellationToken: Parâmetro muito útil pra cancelar a operação se ela não for mais necessária (ex.: usuário fechou o app ou apertou o botão "Cancelar").
  • ValueTask<int>: É uma "promessa" de que, quando a operação terminar, vai retornar um inteiro (int) com o número de bytes lidos. ValueTask é uma versão otimizada de Task pra cenários onde o resultado pode estar disponível sincronamente ou assincronamente.

Exemplo 1: Leitura assíncrona de arquivo

Imagina que temos um arquivo de texto grande e queremos ler sem bloquear a thread principal. Exemplo básico:

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

class Program
{
    // Função assíncrona para ler o arquivo e contar linhas
    public static async Task<int> CountLinesAsync(string filePath)
    {
        int lineCount = 0;
        // Abrir arquivo de forma assíncrona
        using FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
        using StreamReader reader = new StreamReader(fileStream, Encoding.UTF8);

        string? line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            lineCount++;
        }
        return lineCount;
    }

    static async Task Main()
    {
        string filename = "bigtext.txt";
        int count = await CountLinesAsync(filename);
        Console.WriteLine($"No arquivo {filename} linhas: {count}");
    }
}

Comentários sobre o código:

  • Repare no parâmetro useAsync: true no FileStream. Isso é importante pra verdadeira assincronicidade.
  • Usamos await em ReadLineAsync pra não bloquear a thread enquanto a linha é lida.
  • O método Main agora é assíncrono (C# 7+ permite isso).

Se fosse um app gráfico, durante a leitura do arquivo (enquanto ReadAsync espera os dados do disco) o usuário poderia clicar em botões, rolar a tela e fazer outras coisas, porque a thread de UI não estaria bloqueada. Em um app de console isso fica menos visível, mas o princípio é o mesmo.

3. WriteAsync: escrevendo sem demora

Assim como o ReadAsync, o método WriteAsync permite escrever dados no stream de forma assíncrona. É muito útil quando você tem que gravar muitos dados sem fazer o app esperar a conclusão da escrita no disco.

Assinaturas principais:

public virtual ValueTask WriteAsync(
    byte[] buffer,
    int offset,
    int count,
    CancellationToken cancellationToken = default
)

E usando ReadOnlyMemory<byte> (na escrita não modificamos o buffer):

public virtual ValueTask WriteAsync(
    ReadOnlyMemory<byte> buffer,
    CancellationToken cancellationToken = default
)

Os parâmetros são parecidos com ReadAsync:

  • buffer: Array de byte (ou ReadOnlyMemory<byte>) com os dados a serem escritos.
  • offset: Offset no buffer de onde começar a ler os dados para escrita.
  • count: Quantidade de bytes a escrever.
  • CancellationToken cancellationToken: Pra cancelar a operação.
  • ValueTask: Não retorna valor, já que a quantidade escrita é informada pelo parâmetro count.

Exemplo 2: Escrita assíncrona de arquivo

Agora vamos escrever algo num arquivo — também de forma assíncrona.

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

class Program
{
    public static async Task WriteTestAsync(string filePath)
    {
        using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true);
        using StreamWriter writer = new StreamWriter(fs, Encoding.UTF8);

        for (int i = 0; i < 10000; i++)
        {
            await writer.WriteLineAsync($"Linha número {i}");
        }
    }

    static async Task Main()
    {
        string filename = "testout.txt";
        await WriteTestAsync(filename);
        Console.WriteLine($"Gravação {filename} concluída.");
    }
}

Aqui o loop escreve 10000 linhas, e a thread principal não fica bloqueada: se fosse um app GUI, a interface não ficaria "travada".

Graças ao async e await, nosso app de console agora pode, por exemplo, copiar arquivos e continuar responsivo ao input do usuário (no nosso caso, apertar Enter pra cancelar). Esse é um princípio fundamental pra apps modernos, performáticos e responsivos em C#.

4. Nuances úteis

Visualização: como funciona a leitura/escrita assíncrona


┌───────────────────┐     Start Async Read    ┌────────────────────────────────┐
│Seu código (UI/lógica)│ ─────────────────────→  │  OS/E/S: operação assíncrona   │ 
└─────┬─────────────┘                         └───────┬────────────────────────┘
      │(faz outras coisas)                        │(lê o arquivo, espera o disco)
      │<────────────────────────────────────────→│
      └─ Aguarda Task, recebe resultado  ←──────────┘

Esquema aproximado: enquanto o disco trabalha devagar, seu código pode fazer outras coisas. Só quando os dados forem realmente necessários — a execução espera o Task.

Aplicações práticas: onde isso é realmente necessário?

  • Aplicativos desktop: se seu app lê ou grava coisas grandes (logs, bancos de dados, vídeo), assincronia é must-have. Mesmo numa máquina rápida, o usuário pode abrir um arquivo pela rede e a velocidade ser de tartaruga.
  • Backend ou web apps: dezenas ou até milhares de usuários podem bater no servidor ao mesmo tempo. Se cada thread ficar bloqueada em leitura de arquivos — adeus performance e olá 502 Bad Gateway.
  • Apps móveis: abrir ou gravar arquivo pode demorar — usuário percebe lag. Use assincronia!
  • Qualquer processamento em massa de arquivos: apps que trabalham com coleções de arquivos (compactadores, parsers, analisadores) ganham muito com I/O assíncrono.

Leitura/Escrita síncrona vs assíncrona

Método Bloqueia a thread? Fácil de implementar? Melhor performance? Conforto pra UI/servidores
Síncrono (Read/Write) Sim Sim Não Não
Assíncrono (ReadAsync) Não Quase Sim Sim

5. Nuances e melhores práticas

Buffering continua importante: Mesmo com ReadAsync e WriteAsync, ler ou escrever um byte por vez continua extremamente ineficiente. Assincronia remove bloqueios, mas não acelera magicamente cada byte. Bons tamanhos iniciais de buffer são 4096-8192 bytes; pra arquivos grandes vale testar 65536 ou 131072.

"Asincronia deve estar em todo lugar" (Async All The Way Down): Se você começou a usar async/await em um ponto, normalmente convém estender esse padrão por toda a cadeia de chamadas: C faz algo assíncrono — então C é async Task, B também async Task, e Aasync Task. Caso contrário podem surgir bloqueios e até deadlocks em apps com UI.

Tratamento de exceções: No código assíncrono use os usuais try-catch. Exceções comuns são OperationCanceledException e IOException — trate-as explicitamente.

Liberação de recursos (await using): Para streams e outros objetos IDisposable libere recursos corretamente. Se o tipo implementar IAsyncDisposable, então await using chamará DisposeAsync(); se for só IDisposable — será chamado Dispose().

O que acontece por baixo (resumido): Ao usar await o compilador transforma o método num autômato: a operação é iniciada, o método "pausa", o controle volta pra quem chamou. Quando o resultado fica pronto, o SynchronizationContext (na UI) ou o ThreadPool (em console/servidor) retoma a execução do ponto onde parou. Isso permite que uma thread atenda muitas tarefas "suspensas" sem bloquear.

Pra concluir, programação assíncrona com async e await é uma ferramenta poderosa pra criar apps responsivos e escaláveis. Ela permite que seu código use recursos do sistema de forma eficiente, sem bloquear UI ou threads no servidor. Pode parecer estranho no começo, mas vale a pena aprender! Nas próximas palestras vamos nos aprofundar ainda mais no mundo da assincronia e paralelismo. Até a próxima!

2
Tarefa
C# SELF, nível 42, lição 1
Bloqueado
Gravação assíncrona de um arquivo grande
Gravação assíncrona de um arquivo grande
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION