Acesso a arquivos assíncronos (C#)

Você pode usar o recurso async para acessar arquivos. Usando o recurso async, você pode chamar os métodos assíncronos sem usar retornos de chamada ou dividir seu código em vários métodos ou expressões lambda. Para tornar síncrono um código assíncrono, basta chamar um método assíncrono em vez de um método síncrono e adicionar algumas palavras-chave ao código.

Você pode considerar seguintes motivos para adicionar a assincronia às chamadas de acesso ao arquivo:

  • A assincronia torna os aplicativos de interface do usuário mais responsivos porque o thread de interface do usuário que inicia a operação pode executar outro trabalho. Se o thread de interface do usuário precisar executar o código que leva muito tempo (por exemplo, mais de 50 milissegundos), a interface do usuário poderá congelar até que a E/S seja concluída e o thread da interface do usuário possa processar entradas do mouse e do teclado e outros eventos.
  • A assincronia melhora a escalabilidade do ASP.NET e outros aplicativos baseados em servidor reduzindo a necessidade de threads. Se o aplicativo usar um thread dedicado por resposta e mil solicitações forem tratadas simultaneamente, serão necessários mil threads. As operações assíncronas normalmente não precisam usar um thread durante a espera. Elas podem usar o thread de conclusão de E/S existente rapidamente no final.
  • A latência de uma operação de acesso de arquivo pode ser muito baixa nas condições atuais, mas a latência pode aumentar consideravelmente no futuro. Por exemplo, um arquivo pode ser movido para um servidor que está do outro lado do mundo.
  • A sobrecarga adicional de usar o recurso async é pequena.
  • As tarefas assíncronas podem facilmente ser executadas em paralelo.

Usar classes apropriadas

Os exemplos simples neste tópico demonstram File.WriteAllTextAsync e File.ReadAllTextAsync. Para controle fino sobre as operações de E/S do arquivo, use a classe FileStream, que tem uma opção que faz com que a E/S assíncrona ocorra no nível do sistema operacional. Usando essa opção, você pode evitar o bloqueio de um thread de pool de threads em muitos casos. Para habilitar essa opção, você deve especificar o argumento useAsync=true ou options=FileOptions.Asynchronous na chamada do construtor.

Você não pode usar essa opção com StreamReader e StreamWriter se você os abrir diretamente especificando um caminho de arquivo. No entanto, você poderá usar essa opção se fornecer um Stream que a classe FileStream abriu. Chamadas assíncronas serão mais rápidas em aplicativos de interface do usuário mesmo se um thread de pool de threads estiver bloqueado, porque o thread de interface do usuário não é bloqueado durante a espera.

Gravar texto

Os exemplos a seguir gravam texto em um arquivo. A cada instrução await, o método sai imediatamente. Quando o arquivo de E/S for concluído, o método continuará na instrução após a instrução await. O modificador async é a definição de métodos que usam a instrução await.

Exemplo simples

public async Task SimpleWriteAsync()
{
    string filePath = "simple.txt";
    string text = $"Hello World";

    await File.WriteAllTextAsync(filePath, text);
}

Exemplo de controle finito

public async Task ProcessWriteAsync()
{
    string filePath = "temp.txt";
    string text = $"Hello World{Environment.NewLine}";

    await WriteTextAsync(filePath, text);
}

async Task WriteTextAsync(string filePath, string text)
{
    byte[] encodedText = Encoding.Unicode.GetBytes(text);

    using var sourceStream =
        new FileStream(
            filePath,
            FileMode.Create, FileAccess.Write, FileShare.None,
            bufferSize: 4096, useAsync: true);

    await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
}

O exemplo original tem a instrução await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);, que é uma contração das duas instruções a seguir:

Task theTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
await theTask;

A primeira instrução retorna uma tarefa e faz com que o processamento do arquivo seja iniciado. A segunda instrução com o await faz com que o método saia imediatamente e retorne uma tarefa diferente. Quando o processamento do arquivo é concluído posteriormente, a execução retorna para a instrução após a await.

Ler texto

Os exemplos a seguir leem texto de um arquivo.

Exemplo simples

public async Task SimpleReadAsync()
{
    string filePath = "simple.txt";
    string text = await File.ReadAllTextAsync(filePath);

    Console.WriteLine(text);
}

Exemplo de controle finito

O texto é armazenado em buffer e, nesse caso, colocado em um StringBuilder. Diferentemente do exemplo anterior, a avaliação de await produz um valor. O método ReadAsync retorna um Task<Int32>, portanto, a avaliação do await produz um Int32 valor numRead após a conclusão da operação. Para obter mais informações, consulte Tipos de retorno assíncronos (C#).

public async Task ProcessReadAsync()
{
    try
    {
        string filePath = "temp.txt";
        if (File.Exists(filePath) != false)
        {
            string text = await ReadTextAsync(filePath);
            Console.WriteLine(text);
        }
        else
        {
            Console.WriteLine($"file not found: {filePath}");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

async Task<string> ReadTextAsync(string filePath)
{
    using var sourceStream =
        new FileStream(
            filePath,
            FileMode.Open, FileAccess.Read, FileShare.Read,
            bufferSize: 4096, useAsync: true);

    var sb = new StringBuilder();

    byte[] buffer = new byte[0x1000];
    int numRead;
    while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
    {
        string text = Encoding.Unicode.GetString(buffer, 0, numRead);
        sb.Append(text);
    }

    return sb.ToString();
}

E/S assíncrona paralela

Os exemplos a seguir demonstram o processamento paralelo gravando dez arquivos de texto.

Exemplo simples

public async Task SimpleParallelWriteAsync()
{
    string folder = Directory.CreateDirectory("tempfolder").Name;
    IList<Task> writeTaskList = new List<Task>();

    for (int index = 11; index <= 20; ++ index)
    {
        string fileName = $"file-{index:00}.txt";
        string filePath = $"{folder}/{fileName}";
        string text = $"In file {index}{Environment.NewLine}";

        writeTaskList.Add(File.WriteAllTextAsync(filePath, text));
    }

    await Task.WhenAll(writeTaskList);
}

Exemplo de controle finito

Para cada arquivo, o método WriteAsync retorna uma tarefa que é então adicionada a uma lista de tarefas. A instrução await Task.WhenAll(tasks); sai do método e retoma no método quando o processamento do arquivo é concluído para todas as tarefas.

O exemplo fecha todas as instâncias de FileStream em um bloco finally após as tarefas serem concluídas. Se cada FileStream foi criado em uma instrução using, o FileStream pode ter sido descartado antes de a tarefa ter sido concluída.

Qualquer aumento de desempenho é quase que totalmente do processamento paralelo e não o processamento assíncrono. As vantagens da assincronia são que ela não bloqueia vários threads e que ela não bloqueia o thread da interface do usuário.

public async Task ProcessMultipleWritesAsync()
{
    IList<FileStream> sourceStreams = new List<FileStream>();

    try
    {
        string folder = Directory.CreateDirectory("tempfolder").Name;
        IList<Task> writeTaskList = new List<Task>();

        for (int index = 1; index <= 10; ++ index)
        {
            string fileName = $"file-{index:00}.txt";
            string filePath = $"{folder}/{fileName}";

            string text = $"In file {index}{Environment.NewLine}";
            byte[] encodedText = Encoding.Unicode.GetBytes(text);

            var sourceStream =
                new FileStream(
                    filePath,
                    FileMode.Create, FileAccess.Write, FileShare.None,
                    bufferSize: 4096, useAsync: true);

            Task writeTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
            sourceStreams.Add(sourceStream);

            writeTaskList.Add(writeTask);
        }

        await Task.WhenAll(writeTaskList);
    }
    finally
    {
        foreach (FileStream sourceStream in sourceStreams)
        {
            sourceStream.Close();
        }
    }
}

Ao usar os métodos WriteAsync e ReadAsync, você pode especificar um CancellationToken, que pode ser usado para cancelar o fluxo intermediário da operação. Para saber mais, confira Cancelamento em threads gerenciados.

Confira também