CodeGym /Corsi /C# SELF /Lavoro asincrono con file di testo

Lavoro asincrono con file di testo

C# SELF
Livello 42 , Lezione 2
Disponibile

1. Introduzione

Ricorda il nostro esempio con il caricamento di un'immagine grande. Mentre si carica, l'applicazione "si blocca". Lo stesso succede con i file di testo, soprattutto se sono grandi (log da decine di gigabyte, enormi report CSV, backup di database in formato testo).

Immagina di scrivere un'app che:

Parsing di un log enorme per trovare gli errori. Se lo leggi in modo sincrono, la tua interfaccia utente semplicemente "si congela" per qualche secondo o anche minuti, finché l'operazione non termina. L'utente penserà che il programma sia rotto.

Scrive dati in un file di report man mano che vengono generati. Se la scrittura blocca il thread principale, sia la generazione dei dati che l'interazione con l'interfaccia ne risentiranno.

Un web server che deve servire migliaia di richieste. Ogni richiesta potrebbe richiedere lettura o scrittura di file. Se ogni IO su file è sincrono, i thread del server saranno occupati ad aspettare il disco e il server rapidamente "annegherà" sotto il carico di richieste.

In scenari come questi l'IO asincrono non è solo una "bella feature", ma una necessità vitale. Permette alla tua app di non restare inattiva mentre il disco "pensa", ma di fare qualcosa di utile (per esempio aggiornare la UI, processare altre richieste o eseguire calcoli).

Concetti base: async/await e Task

  • La keyword async indica che un metodo può contenere "punti di attesa" (await).
  • L'operatore await temporaneamente cede il controllo finché non completa il task asincrono (per esempio la lettura di un file).
  • Un metodo asincrono esegue IO senza bloccare il thread corrente: mentre non ci sono dati — il thread è libero.

Tutto questo è la base della "magia" asincrona per lavorare con i file.

2. Metodi asincroni per i file

Nelle versioni moderne di .NET praticamente tutte le classi principali per lavorare con i file hanno equivalenti asincroni. Per i file di testo si usano più spesso:

  • StreamReader.ReadLineAsync()
  • StreamReader.ReadToEndAsync()
  • StreamWriter.WriteLineAsync()
  • StreamWriter.WriteAsync()
  • Also i metodi statici: File.ReadAllTextAsync(), File.WriteAllTextAsync() ecc.
Lettura Scrittura
ReadLineAsync()
WriteLineAsync()
ReadToEndAsync()
WriteAsync()
File.ReadAllXAsync()
File.WriteAllXAsync()

3. Lettura asincrona di un intero file di testo

Leggiamo tutto il file in una stringa. Così si fa con file piccoli: config, piccoli log.

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

class Program
{
    static async Task Main()
    {
        string path = "input.txt";
        
        // Leggiamo asincronamente tutto il file
        string fileContents = await File.ReadAllTextAsync(path);
        
        Console.WriteLine("Contenuto del file:");
        Console.WriteLine(fileContents);
    }
}

Nota: il metodo Main ora è marcato come async Task Main(). Questo è possibile a partire da C# 7.1. Un solo await — e tutto funziona in modo asincrono!

4. Lettura asincrona riga per riga di file grandi

Quando il file è davvero grande caricarlo tutto in memoria non è una buona idea. Meglio leggere riga per riga:

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

class Program
{
    static async Task Main()
    {
        string path = "biglog.txt";

        // Apriamo StreamReader per lettura asincrona
        using StreamReader reader = new StreamReader(path);

        string? line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            // Qui puoi processare la riga (per esempio cercare errori)
            Console.WriteLine(line);
        }
    }
}

Come funziona?

Ogni chiamata a await reader.ReadLineAsync() libera il thread — particolarmente utile se il file è su un disco di rete o nel cloud. L'elaborazione asincrona è critica quando ci sono decine di migliaia di righe e lavoro parallelo con gli utenti (per esempio in API server).

5. Scrittura asincrona di righe su file

Analogamente puoi scrivere dati su file in modo asincrono (per esempio durante la generazione di report):

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

class Program
{
    static async Task Main()
    {
        string path = "output.txt";

        using StreamWriter writer = new StreamWriter(path);

        for (int i = 0; i < 5; i++)
        {
            await writer.WriteLineAsync($"Riga numero {i + 1}");
        }
        
        // Puoi chiamare esplicitamente FlushAsync per garantire la scrittura
        await writer.FlushAsync();

        Console.WriteLine("Dati scritti in modo asincrono!");
    }
}

La chiamata a FlushAsync() non è sempre obbligatoria — alla chiusura lo StreamWriter svuoterà il buffer. Ma se vuoi la garanzia "adesso", usala.

6. Interazione di più operazioni asincrone su file

Supponiamo di dover leggere un file di testo e contemporaneamente scrivere una versione trasformata in un altro:

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

class Program
{
    static async Task Main()
    {
        string sourcePath = "even_biggerlog.txt";
        string destinationPath = "copy_biggerlog.txt";

        using StreamReader reader = new StreamReader(sourcePath);
        using StreamWriter writer = new StreamWriter(destinationPath);

        string? line;
        int linesProcessed = 0;

        while ((line = await reader.ReadLineAsync()) != null)
        {
            // Un po' di magia: trasformiamo tutte le lettere in maiuscolo
            string processed = line.ToUpperInvariant();

            await writer.WriteLineAsync(processed);
            linesProcessed++;
        }

        Console.WriteLine($"Righe processate: {linesProcessed}");
    }
}

Qui sia la lettura che la scrittura vengono eseguite in modo asincrono. Ogni await cede il controllo, permettendo all'app di fare altro.

7. Applicazioni pratiche: dove si usa?

  • Sviluppo web (ASP.NET Core): upload/download di file non blocca l'elaborazione di altre richieste; il server resta reattivo.
  • Applicazioni desktop (WPF, WinForms): aprendo un log o salvando un report la UI non "si impalla".
  • Motori di gioco: caricamento asincrono di risorse (texture, modelli) permette di non interrompere animazioni e gameplay.
  • Elaborazione di big data: parsing di grandi CSV/JSON/XML riga per riga, processing "on the fly" senza consumare troppa memoria.
  • Servizi in background e daemon: logging, caching, elaborazione di code con uso efficiente di thread e disco.

Conclusione: l'asincronia aiuta a creare applicazioni moderne, reattive e scalabili. "Blocco" = male, asincronia = bene!

8. Dettagli e best practice

Non dimenticare di await! Se chiami un metodo con suffisso Async senza attendere, otterrai un Task, ma il codice continuerà e questo porterà a errori di ordine di esecuzione.

// MALE: dimenticato await
FileManager.ReadTextFileAsync("nonexistent.txt"); // partirà, ma Main andrà avanti
Console.WriteLine("Mi sono eseguito subito, mentre il file è ancora in lettura (o ha già lanciato un errore)! Questo è male!");

Il compilatore solitamente avvisa per un await mancante, ma non blocca la build.

using per tutto ciò che è IDisposable: tutti gli stream (FileStream, StreamReader, StreamWriter) devono essere rilasciati correttamente. Usa blocchi using o le dichiarazioni using (C# 8+) per chiusura garantita e flush dei buffer.

Dimensione del buffer (bufferSize): StreamReader/StreamWriter sono già ottimizzati, ma per requisiti particolari puoi sperimentare. Di default sono valori comodi (in FileStream spesso si usa 4096 byte).

Gestione degli errori: i metodi asincroni lanciano eccezioni allo stesso modo. Avvolgi le operazioni in try-catch. L'eccezione "emergere" avverrà quando esegui l'await sul relativo Task.

ConfigureAwait: in librerie e scenari web dove non serve il contesto di sincronizzazione (GUI), usa await SomeAsync().ConfigureAwait(false). Questo riduce l'overhead del cambio di contesto. Nelle app console e UI di solito puoi ometterlo.

Pratica — e presto async Task diventerà familiare come Console.WriteLine.

9. Errori tipici e importanti dettagli sulle operazioni asincrone su file

Se non usi await (e semplicemente chiami il metodo con suffisso Async), otterrai un oggetto Task, ma il risultato non verrà atteso automaticamente. Devi aspettarlo con await o attendere esplicitamente (cosa di solito sconsigliata).

Non puoi attendere metodi asincroni da codice sincrono senza "portare" async più in alto nello stack. Usare .Result o .GetAwaiter().GetResult() può causare deadlock — è meglio rendere async i metodi chiamanti.

Non leggere o scrivere lo stesso file contemporaneamente (anche asincronamente). Questo può provocare race e corruzione dei dati.

L'asincronia libera il thread chiamante, ma non rende le operazioni più veloci: se disco o rete sono lenti, saranno comunque lenti — solo senza bloccare UI o thread di lavoro.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION