Accesso ai file asincroni (C#)

È possibile usare la funzionalità Async per accedere ai file. Con la funzionalità Async è possibile chiamare i metodi asincroni senza usare callback o suddividere il codice in più metodi o espressioni lambda. Per rendere asincrono il codice sincrono, è sufficiente chiamare un metodo asincrono anziché un metodo sincrono e aggiungere alcune parole chiave al codice.

È opportuno considerare i seguenti motivi per l'aggiunta della modalità asincrona alle chiamate di accesso ai file:

  • La modalità asincrona rende più reattive le applicazioni dell'interfaccia utente perché il thread dell'interfaccia utente che avvia l'operazione può eseguire altre operazioni. Se il thread dell'interfaccia utente deve eseguire codice che richiede molto tempo (ad esempio, più di 50 millisecondi), l'interfaccia utente può bloccarsi fino al completamento dell'I/O e il thread dell'interfaccia utente può nuovamente elaborare l'input di tastiera e mouse e altri eventi.
  • La modalità asincrona migliora la scalabilità di ASP.NET e di altre applicazioni basate su server, riducendo la necessità di thread. Se l'applicazione usa un thread dedicato per ogni risposta e vengono gestite contemporaneamente mille richieste, sono necessari mille thread. Le operazioni asincrone non richiedono spesso l'uso di un thread durante l'attesa. Usano brevemente il thread di completamento di I/O esistente alla fine.
  • La latenza di un'operazione di accesso ai file può essere molto bassa nelle condizioni attuali, ma aumentare notevolmente in futuro. Ad esempio, un file può essere spostato in tutt'altra parte del mondo.
  • Il sovraccarico aggiuntivo dovuto all'uso della funzionalità Async è ridotto.
  • Le attività asincrone possono essere facilmente eseguite in parallelo.

Usare le classi appropriate

Gli esempi semplici in questo argomento illustrano File.WriteAllTextAsync e File.ReadAllTextAsync. Per controllare correttamente le operazioni di I/O del file, usa la classe FileStream, che ha un'opzione che determina la generazione di I/O asincrono a livello di sistema operativo. L'opzione consente di evitare il blocco di un thread del pool di thread in molti casi. Per abilitare questa opzione, specificare l'argomento useAsync=true o options=FileOptions.Asynchronous nella chiamata al costruttore.

Non è possibile usare questa opzione con oggetti StreamReader e StreamWriter se questi vengono aperti direttamente tramite l'indicazione di un percorso. È tuttavia possibile usare questa opzione se si fornisce loro un oggetto Stream aperto dalla classe FileStream. Le chiamate asincrone sono più veloci nelle applicazioni dell'interfaccia utente anche se un thread del pool di thread è bloccato, poiché il thread dell'interfaccia utente non è bloccato durante l'attesa.

Scrivere testo

Negli esempi seguenti viene scritto il testo in un file. Ad ogni istruzione await il metodo termina immediatamente. Completato l'I/O del file, il metodo riprende in corrispondenza dell'istruzione che segue l'istruzione await. Il modificatore async si trova nella definizione dei metodi che usano l'istruzione await.

Esempio semplice

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

    await File.WriteAllTextAsync(filePath, text);
}

Esempio di controllo 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);
}

L'esempio originale usa l'istruzione await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);, che è una contrazione delle due istruzioni seguenti:

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

La prima istruzione restituisce un'attività e determina l'avvio dell'elaborazione dei file. La seconda istruzione con await induce il metodo a terminare immediatamente e restituire un'attività diversa. Al termine dell'elaborazione dei file l'esecuzione torna quindi all'istruzione che segue await.

Leggi il testo

Negli esempi seguenti viene letto del testo da un file.

Esempio semplice

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

    Console.WriteLine(text);
}

Esempio di controllo finito

Il testo viene memorizzato nel buffer e, in questo caso, inserito in un oggetto StringBuilder. A differenza dell'esempio precedente, la valutazione di await produce un valore. Il metodo ReadAsync restituisce un Task<Int32>>. Pertanto la valutazione dell'attesa produce un valore Int32 (numRead) dopo il completamento dell'operazione. Per altre informazioni, vedere Tipi restituiti asincroni (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();
}

I/O asincrono parallelo

Negli esempi seguenti viene illustrata l'elaborazione parallela per la scrittura di 10 file di testo.

Esempio semplice

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);
}

Esempio di controllo finito

Per ogni file, il metodo WriteAsync restituisce un'attività che successivamente viene aggiunta a un elenco di attività. L'istruzione await Task.WhenAll(tasks); termina il metodo e riprende all'interno del metodo quando l'elaborazione dei file è completata per tutte le attività.

L'esempio chiude tutte le istanze di FileStream in un blocco finally dopo il completamento delle attività. Se invece ogni oggetto FileStream è stato creato in un'istruzione using, è possibile che l'oggetto FileStream venga eliminato prima del completamento dell'attività.

Qualsiasi miglioramento delle prestazioni è dovuto quasi interamente all'elaborazione parallela e non all'elaborazione asincrona. I vantaggi dell'asincronia sono che l'elaborazione non blocca più thread e non blocca il thread dell'interfaccia utente.

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();
        }
    }
}

Quando si usano i metodi WriteAsync e ReadAsync, è possibile specificare uno struct CancellationToken, che consente di annullare l'operazione nel corso del flusso. Per altre informazioni, vedere Annullamento in thread gestiti.

Vedi anche