Gestione delle eccezioni dalle attività Java ExecutorService


213

Sto cercando di utilizzare la ThreadPoolExecutorclasse Java per eseguire un gran numero di attività pesanti con un numero fisso di thread. Ognuna delle attività ha molti luoghi durante i quali potrebbe fallire a causa di eccezioni.

Ho eseguito la sottoclasse ThreadPoolExecutore ho ignorato il afterExecutemetodo che dovrebbe fornire eventuali eccezioni non rilevate durante l'esecuzione di un'attività. Tuttavia, non riesco a farlo funzionare.

Per esempio:

public class ThreadPoolErrors extends ThreadPoolExecutor {
    public ThreadPoolErrors() {
        super(  1, // core threads
                1, // max threads
                1, // timeout
                TimeUnit.MINUTES, // timeout units
                new LinkedBlockingQueue<Runnable>() // work queue
        );
    }

    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if(t != null) {
            System.out.println("Got an error: " + t);
        } else {
            System.out.println("Everything's fine--situation normal!");
        }
    }

    public static void main( String [] args) {
        ThreadPoolErrors threadPool = new ThreadPoolErrors();
        threadPool.submit( 
                new Runnable() {
                    public void run() {
                        throw new RuntimeException("Ouch! Got an error.");
                    }
                }
        );
        threadPool.shutdown();
    }
}

L'output di questo programma è "Va tutto bene - situazione normale!" anche se l'unico Runnable inviato al pool di thread genera un'eccezione. Qualche idea su cosa sta succedendo qui?

Grazie!


non hai mai interrogato il futuro del compito, cosa è successo lì. L'intero programma di esecuzione o programma non verrà arrestato in modo anomalo. L'eccezione viene rilevata e racchiusa in ExecutionException. E verrà riproposto se chiami future.get (). PS: The future.isDone () [Leggere il vero nome dell'API] restituirà true, anche quando il eseguibile è terminato erroneamente. Perché il compito è fatto per davvero.
Jai Pandit

Risposte:


156

Dai documenti :

Nota: quando le azioni sono racchiuse in attività (come FutureTask) in modo esplicito o tramite metodi come submit, questi oggetti task catturano e mantengono eccezioni computazionali e quindi non causano la chiusura improvvisa e le eccezioni interne non vengono passate a questo metodo .

Quando invii un Runnable, verrà racchiuso in un futuro.

Il tuo afterExecute dovrebbe essere qualcosa del genere:

public final class ExtendedExecutor extends ThreadPoolExecutor {

    // ...

    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                Future<?> future = (Future<?>) r;
                if (future.isDone()) {
                    future.get();
                }
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
        if (t != null) {
            System.out.println(t);
        }
    }
}

7
Grazie, ho finito per usare questa soluzione. Inoltre, nel caso qualcuno fosse interessato: altri hanno suggerito di non sottoclassare ExecutorService, ma l'ho fatto comunque perché volevo monitorare le attività mentre si completano piuttosto che aspettare che terminino tutte e quindi chiamare get () su tutti i Futures restituiti .
Tom,

1
Un altro approccio alla sottoclasse dell'esecutore è quello di sottoclassare FutureTask e sovrascrivere il suo metodo "fatto"
nn

1
Tom >> Per favore, puoi pubblicare il tuo codice del frammento di esempio in cui hai
eseguito la

1
Questa risposta non funzionerà se si utilizza ComplableFuture.runAsync poiché afterExecute conterrà un oggetto pacchetto privato e non è possibile accedere al lancio. L'ho aggirato avvolgendo la chiamata. Vedi la mia risposta qui sotto.
mmm

2
Dobbiamo verificare se il futuro è completato utilizzando future.isDone()? Poiché afterExecuteviene eseguito dopo il Runnablecompletamento, presumo future.isDone()ritorni sempre true.
Searene,

247

ATTENZIONE : Va notato che questa soluzione bloccherà il thread chiamante.


Se si desidera elaborare le eccezioni generate dall'attività, è generalmente preferibile utilizzare Callableanziché Runnable.

Callable.call() è consentito generare eccezioni verificate e queste vengono propagate al thread chiamante:

Callable task = ...
Future future = executor.submit(task);
try {
   future.get();
} catch (ExecutionException ex) {
   ex.getCause().printStackTrace();
}

Se Callable.call()genera un'eccezione, questa verrà racchiusa in una ExecutionExceptione gettata da Future.get().

Questo è probabilmente preferibile alla sottoclasse ThreadPoolExecutor. Ti dà anche l'opportunità di reinviare l'attività se l'eccezione è recuperabile.


5
> Callable.call () è autorizzato a generare eccezioni verificate e queste vengono propagate al thread chiamante: si noti che l'eccezione generata si propaga al thread chiamante solo se future.get()viene chiamata la sua versione sovraccarica.
nilato il

16
È perfetto, ma cosa fare se eseguo attività in parallelo e non voglio bloccare l'esecuzione?
Grigory Kislin

44
Non utilizzare questa soluzione, poiché interrompe l'intero scopo dell'utilizzo di ExecutorService. Un ExecutorService è un meccanismo di esecuzione asincrono in grado di eseguire attività in background. Se chiami future.get () subito dopo l'esecuzione, bloccherà il thread chiamante fino al termine dell'attività.
user1801374,

2
Questa soluzione non dovrebbe essere così votata. Future.get () funziona in modo sincrono e fungerà da blocco fino a quando il Runnable o il Callable non saranno stati eseguiti e, come detto sopra, sconfigge lo scopo di utilizzare il Servizio Executor
Super Hans,

2
Come sottolineato da #nhylated, questo merita un JDK BUG. Se Future.get () non viene chiamato, qualsiasi eccezione non rilevata da Callable viene silenziosamente ignorata. Design molto brutto .... ho appena trascorso 1 giorno in più per capire che una libreria ha usato questo e jdk ha ignorato silenziosamente le eccezioni. E questo esiste ancora in jdk12.
Ben Jiang,

18

La spiegazione di questo comportamento è proprio nel javadoc per afterExecute :

Nota: quando le azioni sono racchiuse in attività (come FutureTask) in modo esplicito o tramite metodi come submit, questi oggetti task catturano e mantengono eccezioni computazionali e quindi non causano la chiusura improvvisa e le eccezioni interne non vengono passate a questo metodo .


10

L'ho aggirato avvolgendo il runnable fornito inviato all'esecutore.

CompletableFuture.runAsync(() -> {
        try {
              runnable.run();
        } catch (Throwable e) {
              Log.info(Concurrency.class, "runAsync", e);
        }
}, executorService);

3
È possibile migliorare la leggibilità utilizzando il whenComplete()metodo di CompletableFuture.
Eduard Wirch,

@EduardWirch funziona ma non è possibile restituire un'eccezione da whenComplete ()
Akshat

7

Sto usando la VerboseRunnableclasse da jcabi-log , che ingoia tutte le eccezioni e le registra. Molto conveniente, ad esempio:

import com.jcabi.log.VerboseRunnable;
scheduler.scheduleWithFixedDelay(
  new VerboseRunnable(
    Runnable() {
      public void run() { 
        // the code, which may throw
      }
    },
    true // it means that all exceptions will be swallowed and logged
  ),
  1, 1, TimeUnit.MILLISECONDS
);

3

Un'altra soluzione sarebbe quella di utilizzare ManagedTask e ManagedTaskListener .

È necessario un Callable o Runnable che implementa l'interfaccia ManagedTask .

Il metodo getManagedTaskListenerrestituisce l'istanza desiderata.

public ManagedTaskListener getManagedTaskListener() {

E implementate in ManagedTaskListener il taskDonemetodo:

@Override
public void taskDone(Future<?> future, ManagedExecutorService executor, Object task, Throwable exception) {
    if (exception != null) {
        LOGGER.log(Level.SEVERE, exception.getMessage());
    }
}

Ulteriori dettagli sul ciclo di vita e sul listener delle attività gestite .


2

Questo funziona

  • È derivato da SingleThreadExecutor, ma è possibile adattarlo facilmente
  • Java 8 codice lamdas, ma facile da risolvere

Creerà un Executor con un singolo thread, che può ottenere molte attività; e attenderà che quello corrente termini l'esecuzione con il prossimo

In caso di errore o eccezione di Uncaugth, uncaughtExceptionHandler lo catturerà

classe finale pubblica SingleThreadExecutorWithExceptions {

    public static ExecutorService newSingleThreadExecutorWithExceptions (final Thread.UncaughtExceptionHandler uncaughtExceptionHandler) {

        ThreadFactory factory = (eseguibile eseguibile) -> {
            thread finale newThread = nuovo thread (runnable, "SingleThreadExecutorWithExceptions");
            newThread.setUncaughtExceptionHandler ((final Thread caugthThread, Lanciatore finale gettabile) -> {
                uncaughtExceptionHandler.uncaughtException (caugthThread, lanciobile);
            });
            return newThread;
        };
        restituisce il nuovo FinalizableDelegatedExecutorService
                (nuovo ThreadPoolExecutor (1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        nuovo LinkedBlockingQueue (),
                        fabbrica){


                    protetto vuoto afterExecute (Runnable runnable, Throwable lancable) {
                        super.afterExecute (eseguibile, gettabile);
                        if (gettabile == null && istanza eseguibile di Future) {
                            provare {
                                Future future = (Future) eseguibile;
                                if (future.isDone ()) {
                                    future.get ();
                                }
                            } catch (CancelException ce) {
                                gettabile = ce;
                            } catch (ExecutionException ee) {
                                throwable = ee.getCause ();
                            } catch (InterruptedException ie) {
                                Thread.currentThread () interrupt ().; // ignora / ripristina
                            }
                        }
                        if (gettabile! = null) {
                            uncaughtExceptionHandler.uncaughtException (Thread.currentThread (), throwable);
                        }
                    }
                });
    }



    classe statica privata FinalizableDelegatedExecutorService
            estende DelegatedExecutorService {
        FinalizableDelegatedExecutorService (esecutore ExecutorService) {
            super (esecutore);
        }
        protetto void finalize () {
            super.shutdown ();
        }
    }

    / **
     * Una classe wrapper che espone solo i metodi ExecutorService
     * di un'implementazione di ExecutorService.
     * /
    classe statica privata DelegatedExecutorService estende AbstractExecutorService {
        Servizio esecutivo privato finale e;
        DelegatedExecutorService (ExecutorServiceecutor) {e = esecutore; }
        public void execute (comando eseguibile) {e.execute (comando); }
        public void shutdown () {e.shutdown (); }
        Elenco pubblico shutdownNow () {return e.shutdownNow (); }
        pubblico booleano isShutdown () {return e.isShutdown (); }
        pubblico booleano isTerminated () {return e.isTerminated (); }
        public boolean awaitTermination (timeout lungo, unità TimeUnit)
                genera InterruptedException {
            return e.awaitTermination (timeout, unità);
        }
        public Future submit (attività eseguibile) {
            return e.submit (task);
        }
        public Future submit (attività richiamabile) {
            return e.submit (task);
        }
        public Future submit (attività eseguibile, risultato T) {
            return e.submit (task, risultato);
        }
        Elenco pubblico> invokeAll (Collection> attività)
                genera InterruptedException {
            return e.invokeAll (attività);
        }
        Elenco pubblico> invokeAll (Collection> task,
                                             timeout lungo, unità TimeUnit)
                genera InterruptedException {
            return e.invokeAll (attività, timeout, unità);
        }
        public T invokeAny (Collection> attività)
                genera InterruptedException, ExecutionException {
            return e.invokeAny (attività);
        }
        public T invokeAny (Collection> task,
                               timeout lungo, unità TimeUnit)
                genera InterruptedException, ExecutionException, TimeoutException {
            return e.invokeAny (attività, timeout, unità);
        }
    }



    private SingleThreadExecutorWithExceptions () {}
}

Sfortunatamente l'uso di finalize è un po 'instabile, dal momento che verrà chiamato "più tardi quando il garbage collector lo raccoglie" (o forse non nel caso di un thread, non lo so) ...
rogerdpack,

1

Se si desidera monitorare l'esecuzione dell'attività, è possibile eseguire 1 o 2 thread (forse più a seconda del carico) e utilizzarli per eseguire attività da un wrapper ExecutionCompletionService.


0

Se il tuo ExecutorServiceproviene da una fonte esterna (cioè non è possibile sottoclassare ThreadPoolExecutore sovrascrivere afterExecute()), puoi usare un proxy dinamico per ottenere il comportamento desiderato:

public static ExecutorService errorAware(final ExecutorService executor) {
    return (ExecutorService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
            new Class[] {ExecutorService.class},
            (proxy, method, args) -> {
                if (method.getName().equals("submit")) {
                    final Object arg0 = args[0];
                    if (arg0 instanceof Runnable) {
                        args[0] = new Runnable() {
                            @Override
                            public void run() {
                                final Runnable task = (Runnable) arg0;
                                try {
                                    task.run();
                                    if (task instanceof Future<?>) {
                                        final Future<?> future = (Future<?>) task;

                                        if (future.isDone()) {
                                            try {
                                                future.get();
                                            } catch (final CancellationException ce) {
                                                // Your error-handling code here
                                                ce.printStackTrace();
                                            } catch (final ExecutionException ee) {
                                                // Your error-handling code here
                                                ee.getCause().printStackTrace();
                                            } catch (final InterruptedException ie) {
                                                Thread.currentThread().interrupt();
                                            }
                                        }
                                    }
                                } catch (final RuntimeException re) {
                                    // Your error-handling code here
                                    re.printStackTrace();
                                    throw re;
                                } catch (final Error e) {
                                    // Your error-handling code here
                                    e.printStackTrace();
                                    throw e;
                                }
                            }
                        };
                    } else if (arg0 instanceof Callable<?>) {
                        args[0] = new Callable<Object>() {
                            @Override
                            public Object call() throws Exception {
                                final Callable<?> task = (Callable<?>) arg0;
                                try {
                                    return task.call();
                                } catch (final Exception e) {
                                    // Your error-handling code here
                                    e.printStackTrace();
                                    throw e;
                                } catch (final Error e) {
                                    // Your error-handling code here
                                    e.printStackTrace();
                                    throw e;
                                }
                            }
                        };
                    }
                }
                return method.invoke(executor, args);
            });
}

0

Ciò è dovuto AbstractExecutorService :: submitsta avvolgendo il runnablein RunnableFuture(solo FutureTask) come qui di seguito

AbstractExecutorService.java

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null); /////////HERE////////
    execute(ftask);
    return ftask;
}

Quindi executelo passerà a Workere Worker.run()chiamerà il seguito.

ThreadPoolExecutor.java

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();           /////////HERE////////
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

Alla fine task.run();nella chiamata in codice sopra chiamerà FutureTask.run(). Ecco il codice del gestore delle eccezioni, per questo motivo NON si ottiene l'eccezione prevista.

class FutureTask<V> implements RunnableFuture<V>

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {   /////////HERE////////
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

0

Questo è simile alla soluzione di mmm, ma un po 'più comprensibile. Chiedi alle tue attività di estendere una classe astratta che avvolge il metodo run ().

public abstract Task implements Runnable {

    public abstract void execute();

    public void run() {
      try {
        execute();
      } catch (Throwable t) {
        // handle it  
      }
    }
}


public MySampleTask extends Task {
    public void execute() {
        // heavy, error-prone code here
    }
}

-4

Invece di sottoclassare ThreadPoolExecutor, gli fornirei un'istanza ThreadFactory che crea nuovi thread e fornisce loro un UncaughtExceptionHandler


3
Ho provato anche questo, ma il metodo uncaughtException non sembra mai essere chiamato. Credo che ciò avvenga perché un thread di lavoro nella classe ThreadPoolExecutor sta rilevando le eccezioni.
Tom,

5
Il metodo uncaughtException non viene chiamato perché il metodo di invio di ExecutorService sta eseguendo il wrapping di Callable / Runnable in un futuro; l'eccezione viene catturata lì.
Emil Sit,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.