Acesso assíncrono a ficheiros (C#)

Ao usar a funcionalidade assíncrona para aceder a ficheiros, pode aceder a métodos assíncronos sem usar callbacks ou dividir o seu código em múltiplos 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 assíncronia às chamadas de acesso a ficheiros por estas razões:

  • A assíncronia torna as aplicações de interface mais responsivas porque o thread de interface que inicia a operação pode realizar outros trabalhos. Se a thread da interface tiver de executar código que demora muito tempo (por exemplo, mais de 50 milissegundos), a interface pode congelar até que a E/S esteja completa e a thread da interface possa novamente processar entradas de teclado e rato e outros eventos.
  • A assíncronia melhora a escalabilidade de ASP.NET e de outras aplicações baseadas em servidor ao reduzir a necessidade de threads. Se a aplicação usar uma linha de execução dedicada por resposta e mil requisições estiverem a ser tratadas simultaneamente, são necessárias mil linhas de execução. Operações assíncronas muitas vezes não precisam de usar um thread durante a espera. Usam brevemente a thread de conclusão de I/O preexistente no final do processo.
  • A latência de uma operação de acesso a ficheiros pode ser muito baixa nas condições atuais, mas pode aumentar bastante no futuro. Por exemplo, um ficheiro pode ser movido para um servidor do outro lado do mundo.
  • A sobrecarga adicional de usar a funcionalidade Assíncrona é pequena.
  • Múltiplas operações de E/S assíncronas podem ser executadas sem bloquear a thread que fez a chamada.

Utilize classes apropriadas

Os exemplos simples neste tópico demonstram File.WriteAllTextAsync e File.ReadAllTextAsync. Para um controlo fino sobre as operações de I/O de ficheiros, use a FileStream classe, que tem uma opção que faz com que ocorram I/S assíncronas ao nível do sistema operativo. Ao usar esta opção, pode evitar bloquear um thread pool em muitos casos. Para ativar esta opção, especifique o argumento useAsync=true ou options=FileOptions.Asynchronous na chamada do construtor.

Não podes usar esta opção com StreamReader e StreamWriter se os abrires diretamente especificando um caminho de ficheiro. No entanto, podes usar esta opção se lhes Stream indicares que a FileStream aula abriu. Chamadas assíncronas são mais rápidas em aplicações UI mesmo que um thread pool esteja bloqueado, porque o thread UI não está bloqueado durante a espera.

Escrever texto

Os exemplos seguintes escrevem texto num ficheiro. A cada declaração await, o método é encerrado imediatamente. Quando a E/S do ficheiro está concluída, o método retoma na instrução que se segue à instrução await. O modificador assíncrono está na definição dos métodos que utilizam 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 controlo 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 afirmação await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);, que é uma contração das seguintes duas afirmações:

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

A primeira instrução devolve uma tarefa e provoca o início do processamento de ficheiros. A segunda instrução com o await faz com que o método saia imediatamente e retorne uma tarefa diferente. Quando o processamento do ficheiro termina posteriormente, a execução regressa à instrução que segue o await.

Ler o texto

Os exemplos seguintes leem texto de um ficheiro.

Exemplo simples

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

    Console.WriteLine(text);
}

Exemplo de controlo finito

O texto é armazenado em buffer e, neste caso, colocado num StringBuilder. Ao contrário do exemplo anterior, a avaliação do await produz um valor. O ReadAsync método devolve um Task<Int32>, pelo que a avaliação do await produz um Int32 valor numRead após a conclusão da operação. Para 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();
}

Múltiplas operações assíncronas de I/O

Os exemplos seguintes iniciam múltiplas operações de escrita assíncronas. O tempo de execução coloca estas operações em fila, e a implementação subjacente pode usar threads de I/O ou pool de threads assíncronas do sistema operativo (SO), dependendo da plataforma e configuração, pelo que a concorrência real depende do sistema operativo e 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 controlo finito

Para cada ficheiro, o WriteAsync método devolve uma tarefa que é adicionada a uma lista de tarefas. A instrução await Task.WhenAll(tasks); sai do método e reentra nele após o processamento de ficheiros estar concluído para todas as tarefas.

O exemplo fecha todas as instâncias FileStream em um bloco finally após as tarefas estarem concluídas. Se cada um FileStream fosse criado numa using declaração, FileStream poderia ser eliminado antes de a tarefa ser concluída.

A abordagem assíncrona evita bloquear o thread de chamada enquanto a I/O está pendente. Em muitos casos, as melhorias de rendimento dependem do sistema operativo, do hardware e, em algumas plataformas, do comportamento em tempo de execução 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 métodos WriteAsync e ReadAsync, pode especificar CancellationToken para cancelar a operação a meio do fluxo. Para obter mais informações, consulte Cancelamento em threads gerenciados.

Consulte também