CodeGym /Corsi /C# SELF /Lettura e scrittura asincrona di file (

Lettura e scrittura asincrona di file ( ReadAsync/ WriteAsync)

C# SELF
Livello 42 , Lezione 1
Disponibile

1. Introduzione

Immagina di essere il direttore d'orchestra della tua applicazione. Se ogni volta che il violinista accorda il suo strumento (I/O lungo) aspetti che finisca prima di passare agli altri strumenti, tutta l'orchestra resterà ferma. Ma se il violinista dice: "Mi accordo, voi continuate pure, vi faccio un segnale quando sono pronto", — ecco l'asincronia!

Nel mondo di C# e .NET 9 ci sono strumenti pensati per questo tipo di "multitasking senza blocchi". I protagonisti di oggi sono le versioni asincrone di Read e Write, chiamate ReadAsync e WriteAsync.

Questi metodi ti permettono di avviare un'operazione di lettura o scrittura e subito "rilasciare" il flusso di esecuzione corrente, così può fare altro. Quando l'operazione di I/O termina (per esempio, i dati vengono letti o scritti sul disco), il tuo codice "si risveglia" e riprende da dove era rimasto.

Per usare questi metodi ci servono due parole magiche introdotte in C# nel 2012 con la versione 5.0 (e ora, in C# 14, sono ormai parte del linguaggio!):

  • async: è un modificatore che aggiungi al metodo per dire al compilatore: "Dentro questo metodo ci saranno operazioni asincrone e userò await".
  • await: è l'operatore che metti davanti a una chiamata asincrona (come ReadAsync o WriteAsync). Significa: "Avvia l'operazione, ma non aspettare qui la sua fine. Restituisci il controllo al chiamante e torna quando l'operazione è pronta".

Non preoccuparti se ora ti sembrano concetti vaghi. Avremo un livello dedicato a async e await (Livello 58) dove li vedremo in dettaglio. Per ora è importante capire che aiutano a non "bloccare" il thread principale.

2. ReadAsync: leggere con calma

Il metodo ReadAsync permette di leggere dati da uno stream in modo asincrono. Invece di aspettare che i byte vengano letti dal disco, avvii la lettura e passi subito a fare altro.

Questa è la sua firma principale per leggere in un buffer:

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

O, più usata nel C# moderno (e .NET 9), usando Memory<byte>:

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

Analizziamo i parametri:

  • buffer: è un array di byte (o Memory<byte>) dove verranno scritti i dati letti. Ricorda il buffering per le prestazioni: qui si usa allo stesso modo, ma per operazioni asincrone.
  • offset: lo spostamento in buffer dove iniziare a scrivere i byte letti.
  • count: il numero massimo di byte da leggere.
  • CancellationToken cancellationToken: è molto utile per annullare l'operazione se non serve più (per esempio, l'utente ha chiuso l'app o ha premuto il pulsante "Annulla").
  • ValueTask<int>: è una "promessa" che quando l'operazione finirà restituirà un intero (int) pari al numero di byte letti. ValueTask è una versione ottimizzata di Task per casi in cui il risultato può essere disponibile sincronamente o asincronamente.

Esempio 1: Lettura asincrona di un file

Immaginiamo di avere un file di testo grande e vogliamo leggerlo senza bloccare il thread principale. Ecco un esempio base:

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

class Program
{
    // Funzione asincrona per leggere un file e contare le righe
    public static async Task<int> CountLinesAsync(string filePath)
    {
        int lineCount = 0;
        // Apertura asincrona del file
        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($"Nel file {filename} righe: {count}");
    }
}

Commenti sul codice:

  • Nota il parametro useAsync: true su FileStream. È importante per la vera asincronicità.
  • Usiamo await su ReadLineAsync per non bloccare il thread mentre si legge una riga.
  • Il metodo Main è ora asincrono (C# 7+ lo permette).

Se questo fosse un'app grafica, durante la lettura del file (mentre ReadAsync aspetta i dati dal disco) l'utente potrebbe cliccare bottoni, scrollare e fare altre azioni, perché il thread UI non è bloccato. In una console è meno evidente, ma il principio è lo stesso.

3. WriteAsync: scrivere senza indugi

Analogamente a ReadAsync, WriteAsync permette di scrivere dati su uno stream in modo asincrono. È utilissimo quando devi scrivere grandi quantità di dati senza far aspettare l'applicazione la fine dell'operazione su disco.

Firme principali:

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

E usando ReadOnlyMemory<byte> (perché non modifichiamo il buffer durante la scrittura):

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

I parametri sono simili a ReadAsync:

  • buffer: array di byte (o ReadOnlyMemory<byte>) che contiene i dati da scrivere.
  • offset: spostamento nel buffer da cui iniziare a leggere i dati da scrivere.
  • count: numero di byte da scrivere.
  • CancellationToken cancellationToken: per annullare l'operazione.
  • ValueTask: non restituisce valore perché il numero di byte da scrivere è dato dal parametro count.

Esempio 2: Scrittura asincrona di un file

Ora scriviamo qualcosa su file in modo asincrono.

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($"Riga numero {i}");
        }
    }

    static async Task Main()
    {
        string filename = "testout.txt";
        await WriteTestAsync(filename);
        Console.WriteLine($"Scrittura di {filename} completata.");
    }
}

Il ciclo qui scrive 10000 righe e il thread principale non viene bloccato: se fosse un'app GUI, l'interfaccia non "si pianterebbe".

Grazie a async e await, la nostra applicazione console può eseguire un'operazione di copia file rimanendo comunque "reattiva" all'input dell'utente (per esempio, alla pressione di Enter per annullare). Questo è un principio fondamentale per applicazioni moderne, performanti e reattive in C#.

4. Sfumature utili

Visualizzazione: come funziona la lettura/scrittura asincrona


┌───────────────────┐     Start Async Read    ┌────────────────────────────────┐
│Il tuo codice (UI/logica)│ ─────────────────────→  │  OS/E/S: operazione asincrona │ 
└─────┬─────────────┘                         └───────┬────────────────────────┘
      │(fa altro)                                 │(legge file, aspetta il disco)
      │<────────────────────────────────────────→│
      └─ Attende il Task, riceve il risultato ←──────────┘

Schema approssimativo: mentre il disco lavora lentamente, il codice può fare altro. Solo quando i dati servono — l'esecuzione aspetta il Task.

Applicazioni pratiche: dove serve davvero?

  • Applicazioni desktop: se la tua app legge o scrive cose grandi (log, database, video), l'asincronia è un must. Anche se la tua macchina è veloce, l'utente potrebbe aprire un file in rete con velocità da "tartaruga".
  • Backend o web app: decine o centinaia di utenti possono chiamare il server contemporaneamente. Se ogni thread si blocca su letture file — benvenuti rallentamenti e 502 Bad Gateway.
  • App mobile: se aprire o scrivere un file impiega tempo, l'utente sentirà lag. Usiamo l'asincronia!
  • Qualsiasi elaborazione massiva di file: applicazioni che lavorano su collezioni di file (archiver, parser, analyzer) giovano molto dall'I/O asincrono.

Lettura/scrittura sincrona vs asincrona

Metodo Blocca il thread? Facile da implementare? Migliori prestazioni? Comodo per UI/server
Sincrono (Read/Write) No No
Asincrono (ReadAsync) No Quasi

5. Nitty-gritty e best practice

Buffering è ancora importante: Anche con ReadAsync e WriteAsync, leggere o scrivere un byte alla volta resta molto inefficiente. L'asincronia rimuove il blocco, ma non accelera magicamente la lettura di ogni singolo byte. Taglie di buffer buone in partenza sono 4096-8192 byte; per file grandi prova 65536 o 131072.

"Async All The Way Down": Se cominci a usare async/await in un punto, di solito conviene estendere questo approccio lungo tutta la catena di chiamate: se C esegue un'operazione asincrona, C sarà async Task, B anche async Task, e A pure async Task. Altrimenti rischi blocchi e deadlock nelle UI.

Gestione delle eccezioni: Nel codice asincrono usa i normali try-catch. Spesso vedrai OperationCanceledException e IOException — gestiscile esplicitamente.

Rilascio risorse (await using): Per stream e altri oggetti IDisposable libera correttamente le risorse. Se il tipo implementa IAsyncDisposable, allora await using chiamerà DisposeAsync(); se solo IDisposable — verrà chiamato Dispose().

Cosa succede sotto il cofano (breve): Con await il compilatore trasforma il metodo in una macchina a stati: l'operazione parte, il metodo "si mette in pausa", il controllo torna al chiamante. Quando il risultato è pronto, il SynchronizationContext (in UI) o il ThreadPool (in console/server) riprende l'esecuzione dal punto di pausa. Questo permette a un singolo thread di gestire molte "task sospese" senza blocchi.

In conclusione, la programmazione asincrona con async e await è uno strumento potente per creare applicazioni reattive e scalabili. Permette al tuo codice di usare le risorse di sistema in modo efficiente, senza bloccare l'interfaccia utente o i thread sul server. All'inizio può risultare un po' strano, ma vale la pena impararlo! Nelle lezioni successive ci immergeremo più a fondo nell'asincronia e nel parallelismo. A presto!

2
Compito
C# SELF, livello 42, lezione 1
Bloccato
Scrittura asincrona di un file grande
Scrittura asincrona di un file grande
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION