CodeGym /Corsi /JAVA 25 SELF /Gestione degli errori nell’I/O asincrono, annullamento de...

Gestione degli errori nell’I/O asincrono, annullamento delle operazioni

JAVA 25 SELF
Livello 56 , Lezione 3
Disponibile

1. Gestione degli errori nelle operazioni asincrone

Nel codice sincrono è tutto semplice: se il file non viene trovato o non c’è accesso, intercettate subito l’eccezione in try-catch. Nel codice asincrono, soprattutto quando usate callback (CompletionHandler), l’errore può verificarsi dopo che il vostro metodo è già terminato — da qualche parte nelle profondità del pool di thread. Se non lo gestite correttamente, il programma può comportarsi in modo imprevedibile: dalla perdita “silenziosa” di dati al crash dell’intera applicazione.

Come vengono propagate le eccezioni a CompletionHandler?

Nell’interfaccia CompletionHandler<V, A> ci sono due metodi:

  • completed(V result, A attachment) — viene chiamato se l’operazione è andata a buon fine.
  • failed(Throwable exc, A attachment) — viene chiamato se si è verificato un errore.

Ecco un esempio d’uso:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncErrorDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("nonexistent.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Letti con successo " + result + " byte");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Errore di lettura del file: " + exc.getMessage());
                    // Si può fare logging, notificare l'utente, propagare l'errore più in alto
                }
            });
        } catch (IOException ex) {
            System.out.println("Errore di apertura del file: " + ex.getMessage());
        }

        // Diamo il tempo all'operazione asincrona di terminare (nelle applicazioni reali usare CountDownLatch o altri meccanismi)
        Thread.sleep(500);
    }
}

Che cosa succede qui?

  • Se il file non esiste, il metodo failed verrà invocato con la relativa eccezione (NoSuchFileException).
  • Se l’operazione si conclude con successo, verrà invocato completed.

Esempi di errori tipici

  • File non trovato: NoSuchFileException
  • Accesso negato: AccessDeniedException
  • Errore di lettura/scrittura: vari sottotipi di IOException
  • Problemi con il buffer: BufferOverflowException, BufferUnderflowException

Logging e informazione all’utente

Un errore in un callback asincrono non è un motivo per farsi prendere dal panico, ma neppure per far finta che non sia successo nulla. Una buona pratica è registrare l’errore (per esempio tramite Logger) e, se rilevante per l’utente, mostrare un messaggio o invocare un handler nell’UI.

Esempio di logging:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("Errore dell'operazione asincrona: " + exc);
    exc.printStackTrace();
}

Nel codice di produzione usate logger adeguati (ad esempio java.util.logging o Log4j), non System.err.

2. Annullamento delle operazioni asincrone

Quando può servire annullare un’operazione?

A volte un task asincrono va fermato nel mezzo del lavoro. Per esempio, l’utente ha cambiato idea e ha premuto “Annulla” durante il download di un file. Oppure la finestra del programma è stata chiusa e l’operazione non ha più senso. Succede anche che, alla chiusura dell’applicazione, sia semplicemente necessario rilasciare correttamente le risorse.

Per questi casi l’I/O asincrono in Java supporta l’annullamento tramite l’interfaccia Future. Grazie ad essa si può interrompere in qualsiasi momento un task in esecuzione e non sprecare risorse inutilmente.

Come annullare un’operazione con Future?

Il metodo read o write in AsynchronousFileChannel restituisce un oggetto Future<Integer>. Questo oggetto espone il metodo cancel(boolean mayInterruptIfRunning).

Esempio: annullare una lettura asincrona

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Aspettiamo un attimo e poi annulliamo l'operazione
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Operazione di lettura annullata!");
            } else {
                System.out.println("Impossibile annullare l'operazione (forse è già terminata)");
            }
        }
    }
}

Punti importanti:

  • L’annullamento funziona solo per le operazioni non ancora terminate.
  • Se l’operazione è già finita, non è possibile annullarla.
  • Dopo l’annullamento, tentando di chiamare get() su quel Future, verrà lanciata l’eccezione CancellationException.

Quando non è più possibile annullare un’operazione?

Se il task è già terminato — sia con successo sia con errore — fermarlo non è più possibile, ormai è troppo tardi.

Inoltre, non tutte le implementazioni sono davvero in grado di interrompere le operazioni a livello di sistema operativo. Per esempio, con alcuni file system l’“annullamento” sarà puramente simbolico: l’operazione continuerà a essere eseguita, ma il risultato verrà semplicemente ignorato.

3. Pratica: gestione degli errori e annullamento

Esempio 1: gestione dell’errore durante la lettura di un file inesistente

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;

public class AsyncErrorExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("no_such_file.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Operazione completata con successo");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Errore durante la lettura del file: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("Errore durante l'apertura del file: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

Cosa vedremo in console?

Errore durante l'apertura del file: no_such_file.txt

oppure, se l’errore si verifica in fase di lettura e non di apertura:

Errore durante la lettura del file: NoSuchFileException - no_such_file.txt

Esempio 2: annullamento di un’operazione lunga e chiusura corretta

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 MB

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Dopo 50 ms annulliamo l'operazione (per l'esperimento)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("L'operazione di lettura è stata annullata!");
            } else {
                System.out.println("Impossibile annullare l'operazione (probabilmente è già terminata)");
            }

            try {
                // Proviamo a ottenere il risultato (lancerà CancellationException)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("Catturata CancellationException: l'operazione è davvero annullata.");
            }
        }
    }
}

4. Best practices: come fare nel modo corretto

Rilasciate le risorse anche in caso di errore

Usate try-with-resources per la chiusura automatica dei canali:

try (AsynchronousFileChannel channel = /* open channel */ null) {
    // ...
}

Se usate CompletionHandler, non dimenticate di chiudere il canale al termine di tutte le operazioni. Questo è particolarmente importante se eseguite più operazioni asincrone di seguito.

Non bloccate l’UI/il thread principale

Le operazioni asincrone servono per non bloccare il thread principale. Non chiamate future.get() nel thread dell’UI — altrimenti l’asincronia perde di senso.

Registrate tutti gli errori

In CompletionHandler implementate sempre il metodo failed e fate logging (o propagate) tutte le eccezioni.

Verificate il completamento di tutte le operazioni prima di terminare il programma

Se il programma termina prima che l’operazione finisca, il risultato potrebbe andare perso. Per le dimostrazioni in console a volte si ricorre a Thread.sleep(500), ma nelle applicazioni reali usate CountDownLatch, CompletableFuture o altri meccanismi di sincronizzazione.

Non dimenticate l’annullamento

Se un’operazione non serve più (per esempio l’utente ha chiuso la finestra), annullatela tramite Future.cancel. Questo farà risparmiare risorse e velocizzerà la reattività dell’applicazione.

5. Errori tipici nella gestione degli errori e nell’annullamento nell’I/O asincrono

Errore n. 1: Ignorare il metodo failed in CompletionHandler.
Se non implementate la gestione degli errori, la vostra applicazione si comporterà in modo imprevedibile: gli errori si “perderanno” e l’utente non capirà perché non succede nulla.

Errore n. 2: Canale non chiuso dopo il completamento delle operazioni.
Vi siete dimenticati di chiudere AsynchronousFileChannel — avrete perdite di risorse e, forse, il file resterà bloccato dal sistema operativo.

Errore n. 3: Attendere il risultato di un’operazione asincrona nel thread principale.
Avete chiamato future.get() nel thread dell’UI — l’interfaccia si è “congelata” e tutta l’asincronia è andata persa.

Errore n. 4: Tentativo di annullare un’operazione già terminata.
Chiamate cancel() troppo tardi — l’operazione è già terminata e l’annullamento non funzionerà. Non è critico, ma può confondere in fase di debug.

Errore n. 5: Non verificate l’esito dell’annullamento.
Chiamate cancel() ma non controllate il valore restituito e non gestite CancellationException quando chiamate get() — l’applicazione può andare in errore o comportarsi in modo strano.

Errore n. 6: Non rilasciate le risorse in caso di errore o annullamento.
Se non chiudete il canale dopo un errore o un annullamento, possono verificarsi perdite o blocchi dei file.

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