Java: Timeout für einen bestimmten Codeblock festlegen?

73

Ist es möglich, Java zu zwingen, eine Ausnahme auszulösen, nachdem ein Codeblock länger als akzeptabel ausgeführt wurde?

htf
quelle

Antworten:

23

Ja, aber es ist im Allgemeinen eine sehr schlechte Idee, einen anderen Thread zu zwingen, in einer zufälligen Codezeile zu unterbrechen. Sie würden dies nur tun, wenn Sie beabsichtigen, den Prozess herunterzufahren.

Was Sie tun können, ist, Thread.interrupt()nach einer bestimmten Zeit für eine Aufgabe zu verwenden . Wenn der Code dies nicht überprüft, funktioniert es jedoch nicht. Ein ExecutorService kann dies mit vereinfachenFuture.cancel(true)

Es ist viel besser, wenn der Code sich selbst zeitlich festlegt und anhält, wenn es nötig ist.

Peter Lawrey
quelle
1
Das Problem ist, dass ich eine Drittanbieter-Bibliothek habe, die manchmal zu lange läuft und es keinen nativen Mechanismus für das
Timeout gibt
2
Dies ist leider ein häufiges Problem, und die einzige zuverlässige Möglichkeit, dies zu verwalten, besteht darin, einen separaten Prozess zu haben, den Sie beenden können. Die Alternative besteht darin, Thread.stop () darauf zu verwenden. Lesen Sie die Warnungen für diese Methode, bevor Sie sie verwenden!
Peter Lawrey
51

Hier ist der einfachste Weg, den ich kenne, um dies zu tun:

final Runnable stuffToDo = new Thread() {
  @Override 
  public void run() { 
    /* Do stuff here. */ 
  }
};

final ExecutorService executor = Executors.newSingleThreadExecutor();
final Future future = executor.submit(stuffToDo);
executor.shutdown(); // This does not cancel the already-scheduled task.

try { 
  future.get(5, TimeUnit.MINUTES); 
}
catch (InterruptedException ie) { 
  /* Handle the interruption. Or ignore it. */ 
}
catch (ExecutionException ee) { 
  /* Handle the error. Or ignore it. */ 
}
catch (TimeoutException te) { 
  /* Handle the timeout. Or ignore it. */ 
}
if (!executor.isTerminated())
    executor.shutdownNow(); // If you want to stop the code that hasn't finished.

Alternativ können Sie eine TimeLimitedCodeBlock-Klasse erstellen, um diese Funktionalität zu verpacken, und sie dann überall dort verwenden, wo Sie sie benötigen:

new TimeLimitedCodeBlock(5, TimeUnit.MINUTES) { @Override public void codeBlock() {
    // Do stuff here.
}}.run();
user2116890
quelle
Bin gerade darauf gestoßen. Was ist der Aufwand für etwas in diese Richtung? Ich habe das Gefühl, wenn Sie viel in stuffToDo tun, ist es teuer, jedes Mal einen neuen Single-Thread-Executor zu erstellen, aber ich kenne meine Frage nicht.
MarkII
Ich habe bei diesem Ansatz noch nie ein Leistungsproblem festgestellt. In einigen Fällen kann es vorzuziehen sein, eine eigene Implementierung von Executor zu erstellen , wenn Sie der Meinung sind, dass Sie eine leichtere Version erstellen können, indem Sie sich speziell auf den Fall konzentrieren, dass nur ein Thread vorhanden ist.
user2116890
Die Leistung von ExecutorService wurde hier positiv bewertet ( stackoverflow.com/a/27025552/2116890 ), wo festgestellt wurde, dass sie fantastisch ist. In diesem Fall hatten sie es mit mehr Threads zu tun. Nach meiner Erfahrung habe ich, egal ob ich einen oder mehrere Threads verwende, den Overhead nie bemerkt, aber ich habe mich auch nicht bemüht, seine Leistung im Vergleich zu einer Alternative in irgendeiner Weise zu messen.
user2116890
1
Danach executor.shutdownNow()möchten Sie wahrscheinlich while (true) {try {if (executor.awaitTermination(1, TimeUnit.SECONDS)) break;} catch (InterruptedException ie) {}}, da das tatsächliche Stoppen der Aufgabe einige Zeit dauern kann.
Anders Kaseorg
Interessante Sache hier zu beachten. Ein Timeout bedeutet nicht, dass die zugrunde liegende Aufgabe gestoppt wurde. Die Zukunft wird abgebrochen und der executorService wird darüber informiert, aber der Thread arbeitet möglicherweise noch, ohne sich um die Beendigung zu kümmern, was bedeutet, dass möglicherweise noch Ressourcen verschwendet werden, die Sie gerne freigegeben hätten.
Tarmo
41

Ich habe einige der anderen Antworten in einer einzigen Dienstprogrammmethode zusammengefasst:

public class TimeLimitedCodeBlock {

  public static void runWithTimeout(final Runnable runnable, long timeout, TimeUnit timeUnit) throws Exception {
    runWithTimeout(new Callable<Object>() {
      @Override
      public Object call() throws Exception {
        runnable.run();
        return null;
      }
    }, timeout, timeUnit);
  }

  public static <T> T runWithTimeout(Callable<T> callable, long timeout, TimeUnit timeUnit) throws Exception {
    final ExecutorService executor = Executors.newSingleThreadExecutor();
    final Future<T> future = executor.submit(callable);
    executor.shutdown(); // This does not cancel the already-scheduled task.
    try {
      return future.get(timeout, timeUnit);
    }
    catch (TimeoutException e) {
      //remove this if you do not want to cancel the job in progress
      //or set the argument to 'false' if you do not want to interrupt the thread
      future.cancel(true);
      throw e;
    }
    catch (ExecutionException e) {
      //unwrap the root cause
      Throwable t = e.getCause();
      if (t instanceof Error) {
        throw (Error) t;
      } else if (t instanceof Exception) {
        throw (Exception) t;
      } else {
        throw new IllegalStateException(t);
      }
    }
  }

}

Beispielcode, der diese Dienstprogrammmethode verwendet:

  public static void main(String[] args) throws Exception {
    final long startTime = System.currentTimeMillis();
    log(startTime, "calling runWithTimeout!");
    try {
      TimeLimitedCodeBlock.runWithTimeout(new Runnable() {
        @Override
        public void run() {
          try {
            log(startTime, "starting sleep!");
            Thread.sleep(10000);
            log(startTime, "woke up!");
          }
          catch (InterruptedException e) {
            log(startTime, "was interrupted!");
          }
        }
      }, 5, TimeUnit.SECONDS);
    }
    catch (TimeoutException e) {
      log(startTime, "got timeout!");
    }
    log(startTime, "end of main method!");
  }

  private static void log(long startTime, String msg) {
    long elapsedSeconds = (System.currentTimeMillis() - startTime);
    System.out.format("%1$5sms [%2$16s] %3$s\n", elapsedSeconds, Thread.currentThread().getName(), msg);
  }

Ausgabe vom Ausführen des Beispielcodes auf meinem Computer:

    0ms [            main] calling runWithTimeout!
   13ms [ pool-1-thread-1] starting sleep!
 5015ms [            main] got timeout!
 5016ms [            main] end of main method!
 5015ms [ pool-1-thread-1] was interrupted!
Neeme Praks
quelle
Können Sie bitte ein Beispiel zur Verwendung hinzufügen? Trotz meiner Einstellungen wird die Ausnahme ausgelöst, sobald ich die runWithTimeoutMethode ausführe , auch wenn ich das Zeitlimit auf 150 Minuten festgelegt habe.
StepTNT
2
Ich habe jetzt Beispielcode hinzugefügt und den Originalcode verbessert, um die bereits übermittelte Aufgabe im Falle einer Zeitüberschreitung abzubrechen. Ich würde mich über eine positive
Bewertung freuen
@htf, das sollte wirklich die akzeptierte Antwort sein. Schön gemacht
Carlos
1
@ AliReza19330, ja, mein schlechtes. Du hast recht. Das Starten eines neuen Threads pro Benutzer ist jedoch in Bezug auf die Leistung möglicherweise nicht die beste Idee. :-P
Neeme Praks
1
@NeemePraks Ich denke "throw (Exception) e;" sollte "throw (Exception) t;" sein in der zweiten runWithTimeout-Methode
Alb
10

Wenn es sich um Testcode handelt, den Sie zeitlich festlegen möchten, können Sie das folgende timeAttribut verwenden:

@Test(timeout = 1000)  
public void shouldTakeASecondOrLess()
{
}

Wenn es sich um Produktionscode handelt, gibt es keinen einfachen Mechanismus. Welche Lösung Sie verwenden, hängt davon ab, ob Sie den zeitgesteuerten Code ändern können oder nicht.

Wenn Sie den zeitgesteuerten Code ändern können, besteht ein einfacher Ansatz darin, Ihren zeitgesteuerten Code an die Startzeit und in regelmäßigen Abständen an die aktuelle Zeit zu erinnern. Z.B

long startTime = System.currentTimeMillis();
// .. do stuff ..
long elapsed = System.currentTimeMillis()-startTime;
if (elapsed>timeout)
   throw new RuntimeException("tiomeout");

Wenn der Code selbst nicht nach Zeitüberschreitung suchen kann, können Sie den Code in einem anderen Thread ausführen und auf den Abschluss oder die Zeitüberschreitung warten.

    Callable<ResultType> run = new Callable<ResultType>()
    {
        @Override
        public ResultType call() throws Exception
        {
            // your code to be timed
        }
    };

    RunnableFuture future = new FutureTask(run);
    ExecutorService service = Executors.newSingleThreadExecutor();
    service.execute(future);
    ResultType result = null;
    try
    {
        result = future.get(1, TimeUnit.SECONDS);    // wait 1 second
    }
    catch (TimeoutException ex)
    {
        // timed out. Try to stop the code if possible.
        future.cancel(true);
    }
    service.shutdown();
}
mdma
quelle
3

Ich kann zwei Optionen vorschlagen.

  1. Fügen Sie innerhalb der Methode ein lokales Feld hinzu und testen Sie die Zeit jedes Mal um die Schleife, vorausgesetzt, es handelt sich um eine Schleife und wartet nicht auf ein externes Ereignis.

    void method() {
        long endTimeMillis = System.currentTimeMillis() + 10000;
        while (true) {
            // method logic
            if (System.currentTimeMillis() > endTimeMillis) {
                // do some clean-up
                return;
            }
        }
    }
    
  2. Führen Sie die Methode in einem Thread aus und lassen Sie den Anrufer bis 10 Sekunden zählen.

    Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                method();
            }
    });
    thread.start();
    long endTimeMillis = System.currentTimeMillis() + 10000;
    while (thread.isAlive()) {
        if (System.currentTimeMillis() > endTimeMillis) {
            // set an error flag
            break;
        }
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException t) {}
    }
    

Der Nachteil dieses Ansatzes besteht darin, dass method () einen Wert nicht direkt zurückgeben kann, sondern ein Instanzfeld aktualisieren muss, um seinen Wert zurückzugeben.

Farzin Zaker
quelle
1
+0: ​​In Ihrem zweiten Beispiel versuchen Sie eine Thread.join(long)mit einer Auszeit zu machen;)
Peter Lawrey
2
Nachdem thread.start()Sie thread.join(10000);anstelle des restlichen Codes haben könnten .
Peter Lawrey
3

EDIT: Peter Lawrey hat vollkommen recht: Es ist nicht so einfach wie das Unterbrechen eines Threads (mein ursprünglicher Vorschlag), und Executors & Callables sind sehr nützlich ...

Anstatt Threads zu unterbrechen, können Sie eine Variable für Callable festlegen, sobald das Timeout erreicht ist. Der Callable sollte diese Variable an geeigneten Punkten in der Taskausführung überprüfen, um zu wissen, wann sie zu stoppen ist.

Callables geben Futures zurück, mit denen Sie eine Zeitüberschreitung festlegen können, wenn Sie versuchen, das Ergebnis der Zukunft zu erhalten. Etwas wie das:

try {
   future.get(timeoutSeconds, TimeUnit.SECONDS)
} catch(InterruptedException e) {
   myCallable.setStopMeAtAppropriatePlace(true);
}

Siehe Future.get, Executors und Callable ...

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html#get-long-java.util.concurrent.TimeUnit-

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Callable.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newFixedThreadPool%28int%29

laher
quelle
1
Leider funktioniert das nur, wenn Sie den Code steuern, der innerhalb des Callable ausgeführt wird (was, wenn das wahr ist, dann ist dies trivial)
Chii
ok, das kann ich jetzt im Kommentar von @ HTF sehen. Ich denke Thread.stop () ist der einzige Weg! Siehe Warnungen hier: download.oracle.com/javase/6/docs/technotes/guides/concurrency/…
laher
2

Ich habe eine sehr einfache Lösung ohne Verwendung von Frameworks oder APIs erstellt. Das sieht eleganter und verständlicher aus. Die Klasse heißt TimeoutBlock.

public class TimeoutBlock {

 private final long timeoutMilliSeconds;
    private long timeoutInteval=100;

    public TimeoutBlock(long timeoutMilliSeconds){
        this.timeoutMilliSeconds=timeoutMilliSeconds;
    }

    public void addBlock(Runnable runnable) throws Throwable{
        long collectIntervals=0;
        Thread timeoutWorker=new Thread(runnable);
        timeoutWorker.start();
        do{ 
            if(collectIntervals>=this.timeoutMilliSeconds){
                timeoutWorker.stop();
                throw new Exception("<<<<<<<<<<****>>>>>>>>>>> Timeout Block Execution Time Exceeded In "+timeoutMilliSeconds+" Milli Seconds. Thread Block Terminated.");
            }
            collectIntervals+=timeoutInteval;           
            Thread.sleep(timeoutInteval);

        }while(timeoutWorker.isAlive());
        System.out.println("<<<<<<<<<<####>>>>>>>>>>> Timeout Block Executed Within "+collectIntervals+" Milli Seconds.");
    }

    /**
     * @return the timeoutInteval
     */
    public long getTimeoutInteval() {
        return timeoutInteval;
    }

    /**
     * @param timeoutInteval the timeoutInteval to set
     */
    public void setTimeoutInteval(long timeoutInteval) {
        this.timeoutInteval = timeoutInteval;
    }
}

Beispiel:

try {
        TimeoutBlock timeoutBlock = new TimeoutBlock(10 * 60 * 1000);//set timeout in milliseconds
        Runnable block=new Runnable() {

            @Override
            public void run() {
                //TO DO write block of code to execute
            }
        };

        timeoutBlock.addBlock(block);// execute the runnable block 

    } catch (Throwable e) {
        //catch the exception here . Which is block didn't execute within the time limit
    }

Dies war sehr nützlich für mich, als ich mich mit einem FTP-Konto verbinden musste. Dann laden Sie Sachen herunter und laden Sie sie hoch. Manchmal hängt die FTP-Verbindung oder bricht vollständig ab. Dies führte zum Ausfall des gesamten Systems. und ich brauchte einen Weg, um es zu erkennen und zu verhindern, dass es passiert. Also habe ich das erstellt und benutzt. Funktioniert ziemlich gut.

Niroshan Abeywickrama
quelle
1

Wenn Sie eine CompletableFuture-Methode wünschen, können Sie eine Methode wie verwenden

public MyResponseObject retrieveDataFromEndpoint() {

   CompletableFuture<MyResponseObject> endpointCall 
       = CompletableFuture.supplyAsync(() ->
             yourRestService.callEnpoint(withArg1, withArg2));

   try {
       return endpointCall.get(10, TimeUnit.MINUTES);
   } catch (TimeoutException 
               | InterruptedException 
               | ExecutionException e) {
       throw new RuntimeException("Unable to fetch data", e);
   }
}

Wenn Sie spring verwenden, können Sie die Methode mit einem versehen, @Retryablesodass die Methode dreimal wiederholt wird, wenn eine Ausnahme ausgelöst wird.

Tom Chamberlain
quelle
1

Anstatt die Aufgabe im neuen Thread und den Timer im Hauptthread zu haben, sollten Sie den Timer im neuen Thread und die Aufgabe im Hauptthread haben:

public static class TimeOut implements Runnable{
    public void run() {
        Thread.sleep(10000);
        if(taskComplete ==false) {
            System.out.println("Timed Out");
            return;
        }
        else {
            return;
        }
    }
}
public static boolean taskComplete = false;
public static void main(String[] args) {
    TimeOut timeOut = new TimeOut();
    Thread timeOutThread = new Thread(timeOut);
    timeOutThread.start();
    //task starts here
    //task completed
    taskComplete =true;
    while(true) {//do all other stuff }
}
Reuben Varma
quelle
1
Ihre Variable TimeOut timeOutin der main()Methode wird niemals verwendet.
Johannes
2
In der Hauptmethode sollte Thread timeOutThread = new Thread(timeOut);dann @Johannes timeOut verwendet werden.
Arjun Varshney
Beachten Sie, dass dies die einzige skalierbare Lösung ist. Sie brauchen wirklich nur einen einzigen Timer-Thread, und das könnte Tausende von Worker-Threads unterbrechen. Umgekehrt haben Sie Tausende von Timer-Threads für Tausende von Worker-Threads - gelinde gesagt nicht ideal.
Agoston Horvath
0

Es gibt einen hackigen Weg, dies zu tun.

Legen Sie ein boolesches Feld fest, um anzugeben, ob die Arbeit abgeschlossen wurde. Stellen Sie dann vor dem Codeblock einen Timer ein, um nach Ihrer Zeitüberschreitung einen Code auszuführen. Der Timer prüft, ob die Ausführung des Codeblocks abgeschlossen ist, und löst andernfalls eine Ausnahme aus. Sonst wird es nichts tun.

Das Ende des Codeblocks sollte natürlich das Feld auf true setzen, um anzuzeigen, dass die Arbeit erledigt wurde.

HXCaine
quelle
Dies ist nicht 100% korrekt, da der Timer-Thread unabhängig von dem Thread ist, auf dem der Codeblock ausgeführt wird. Der Timer-Thread kann eine Ausnahme auslösen, obwohl der 'Worker'-Thread nichts getan hat, da es andere Threads mit höherer Priorität gab. Es gibt einen kleinen Unterschied zwischen "Läuft länger als X Sekunden" und "Vor X Sekunden gestartet".
Kojotak
Okay, fair genug. Danke für die Antwort. Ich werde die Antwort
offen
0

Ich hatte ein ähnliches Problem, bei dem es meine Aufgabe war, innerhalb eines bestimmten Zeitlimits eine Nachricht an SQS zu senden. Ich habe die triviale Logik verwendet, es über einen anderen Thread auszuführen und auf sein zukünftiges Objekt zu warten, indem ich das Zeitlimit angegeben habe. Dies würde mir bei Timeouts eine TIMEOUT-Ausnahme geben.

final Future<ISendMessageResult> future = 
timeoutHelperThreadPool.getExecutor().submit(() -> {
  return getQueueStore().sendMessage(request).get();
});
try {
  sendMessageResult = future.get(200, TimeUnit.MILLISECONDS);
  logger.info("SQS_PUSH_SUCCESSFUL");
  return true;

} catch (final TimeoutException e) {
  logger.error("SQS_PUSH_TIMEOUT_EXCEPTION");
}

Es gibt jedoch Fälle, in denen Sie nicht verhindern können, dass der Code von einem anderen Thread ausgeführt wird, und in diesem Fall echte Negative erhalten.

Beispiel: In meinem Fall hat meine Anforderung SQS erreicht, und während die Nachricht gesendet wurde, hat meine Codelogik das angegebene Zeitlimit festgestellt. In Wirklichkeit wurde meine Nachricht in die Warteschlange verschoben, aber mein Hauptthread ging davon aus, dass sie aufgrund der TIMEOUT-Ausnahme fehlgeschlagen ist. Dies ist eine Art Problem, das vermieden und nicht gelöst werden kann. Wie in meinem Fall habe ich es vermieden, indem ich eine Zeitüberschreitung angegeben habe, die in fast allen Fällen ausreichen würde.

Wenn sich der Code, den Sie unterbrechen möchten, in Ihrer Anwendung befindet und nicht wie ein API-Aufruf ist, können Sie ihn einfach verwenden

future.cancel(true)

Denken Sie jedoch daran, dass Java Docs angibt, dass die Ausführung blockiert wird.

"Versuche, die Ausführung dieser Aufgabe abzubrechen. Dieser Versuch schlägt fehl, wenn die Aufgabe bereits abgeschlossen wurde, bereits abgebrochen wurde oder aus einem anderen Grund nicht abgebrochen werden konnte. Wenn dies erfolgreich ist und diese Aufgabe beim Abbruch nicht gestartet wurde, wird dies ausgeführt Die Task sollte niemals ausgeführt werden. Wenn die Task bereits gestartet wurde, bestimmt der Parameter mayInterruptIfRunning, ob der Thread, der diese Task ausführt, unterbrochen werden soll, um die Task zu stoppen. "

P3A
quelle