Acesso a arquivos assíncronos (C#)

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

Considere adicionar assincronia às chamadas de acesso a arquivo por estas razões:

  • 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 outras tarefas. Se o thread de interface do usuário precisar executar um código que leve 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 de interface do usuário possa processar novamente a entrada do teclado e do mouse e outros eventos.
  • A assíncrona melhora a escalabilidade de 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 estiverem sendo tratadas simultaneamente, serão necessários mil threads. As operações assíncronas geralmente não precisam usar um thread durante a espera. Eles usam o thread de conclusão de E/S existente brevemente no final.
  • A latência de uma operação de acesso a arquivos pode ser muito baixa nas condições atuais, mas a latência pode aumentar muito no futuro. Por exemplo, um arquivo pode ser movido para um servidor que está em todo o mundo.
  • A sobrecarga adicional de usar o recurso Async é pequena.
  • Várias operações de E/S assíncronas podem ser executadas sem bloquear o thread de chamada.

Utilize classes apropriadas

Os exemplos simples neste tópico demonstram File.WriteAllTextAsync e File.ReadAllTextAsync. Para obter controle fino sobre as operações de E/S do arquivo, use a FileStream classe, 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 bloquear um thread do pool de threads em muitos casos. Para habilitar essa opção, especifique 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 abri-la diretamente especificando um caminho de arquivo. No entanto, você pode usar essa opção se fornecer a eles um Stream que foi aberto pela classe FileStream. As chamadas assíncronas são mais rápidas em aplicativos de interface do usuário mesmo se um thread do pool de threads estiver bloqueado, porque o thread da 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 é imediatamente interrompido. Quando a E/S do arquivo for concluída, o método será retomado na instrução que segue a instrução await. O modificador assíncrono está na 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 seguintes instruções:

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

A primeira instrução retorna uma tarefa e faz com que o processamento de arquivos 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 de arquivo for concluído mais tarde, a execução retornará à instrução seguinte ao 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. Ao contrário do exemplo anterior, a avaliação do await produz um valor. O ReadAsync método retorna um Task<Int32>, portanto, a avaliação do await produz um valor Int32numRead 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();
}

Várias operações assíncronas de E/S

Os exemplos a seguir iniciam várias operações de gravação assíncrona. O runtime enfileira essas operações, e a implementação subjacente pode usar E/S assíncrona do sistema operacional (SO) ou threads do pool de threads, dependendo da plataforma e da configuração; assim, a simultaneidade real depende do sistema operacional e do hardware.

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 WriteAsync método retorna uma tarefa que é adicionada a uma lista de tarefas. A await Task.WhenAll(tasks); instrução sai do método e é retomada dentro do método quando o processamento de arquivos é concluído para todas as tarefas.

O exemplo fecha todas as instâncias FileStream em bloco finally após a conclusão das tarefas. Se cada FileStream for criado em uma instrução using, FileStream poderá ser descartado antes da conclusão da tarefa.

A abordagem assíncrona evita bloquear o thread chamador durante operações de E/S pendentes. Em muitos casos, os aprimoramentos de taxa de transferência dependem do sistema operacional, do hardware e, em algumas plataformas, do comportamento de runtime do .NET, como limites de pool de threads e agendamento.

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 WriteAsync, ReadAsync, você pode especificar um CancellationToken para cancelar a operação a meio do fluxo. Para saber mais, confira Cancelamento em threads gerenciados.

Consulte também