2016-01-04 9 views
7

Ich habe eine Reihe von Code-Blöcken, die zu lange dauern. Ich brauche keine Finesse, wenn es nicht klappt. In der Tat, ich möchte eine Ausnahme auslösen, wenn diese Blöcke zu lange dauern, und nur durch unsere Standard-Fehlerbehandlung fallen. Ich würde es vorziehen, KEINE Methoden aus jedem Block zu erstellen (was die einzigen Vorschläge sind, die ich bisher gesehen habe), da dies eine grundlegende Neuschreibung der Codebasis erfordern würde.Wie erstellt man ein generisches Timeout-Objekt für verschiedene Codeblöcke?

Hier ist, was ich würde LIKE zu erstellen, wenn möglich.

Ich habe ein einfaches Objekt erstellt, um dies mit der Timer-Klasse zu tun. (Hinweis für diejenigen, die kopieren mag/Paste: DIESES Code funktioniert nicht !!)

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Timers; 

    public class MyTimeoutObject : IDisposable 
    { 
     private Timer timer = null; 

     public MyTimeoutObject (TimeSpan ts) 
     { 
      timer = new Timer(); 
      timer.Elapsed += timer_Elapsed; 
      timer.Interval = ts.TotalMilliseconds; 

      timer.Start(); 
     } 

     void timer_Elapsed(object sender, ElapsedEventArgs e) 
     { 
      throw new TimeoutException("A code block has timed out."); 
     } 

     public void Dispose() 
     { 
      if (timer != null) 
      { 
       timer.Stop(); 
      } 
     } 
    } 

Es ist nicht funktioniert, weil die System.Timers.Timer Klasse erfasst, absorbiert und ignoriert alle Ausnahmen geworfen innerhalb, was - wie ich entdeckt habe - mein Design besiegt. Jede andere Möglichkeit, diese Klasse/Funktionalität ohne ein totales Redesign zu erstellen?

Das schien vor zwei Stunden so einfach zu sein, verursacht mir aber viele Kopfschmerzen.

+0

Behandeln Sie Ausnahmen auf der oberen Ebene, von wo Sie diese Methode aufrufen? Ist es außerdem eine Multithread-Umgebung? – Shaharyar

+0

Sie können einen statischen Speicher von 'CancellationToken' verwenden und sie überall dort verwenden, wo Sie eine Aufgabe haben, oder - weniger elegant, aber vielleicht effektiver - einen anderen Prozess haben, der Ihren Hauptprozess beenden kann und über lokale HTTP- oder Named-Pipes aufgerufen wird –

+0

Shaharyar: Ja. Der Fang ist weit oben in der Kette. Und ja, das ist eine Multithread-Umgebung. Die Bibliothek, die ich schreibe, soll in WinForms-Anwendungen und nicht im Web verwendet werden. – Jerry

Antwort

2

OK, ich habe einige Zeit auf diesem einen verbracht und ich denke, ich habe eine Lösung, die für Sie arbeiten wird, ohne dass Sie Ihren Code so viel ändern müssen.

Im folgenden Beispiel verwenden Sie die Klasse Timebox, die ich erstellt habe.

public void MyMethod(...) { 

    // some stuff 

    // instead of this 
    // using(...){ /* your code here */ } 

    // you can use this 
    var timebox = new Timebox(TimeSpan.FromSeconds(1)); 
    timebox.Execute(() => 
    { 
     /* your code here */ 
    }); 

    // some more stuff 

} 

Hier ist, wie Timebox funktioniert.

  • Ein Timebox Objekt ist mit einem Timespan
  • gegeben erstellt Wenn Execute genannt wird, erstellt die Timebox ein Kind AppDomain einen TimeboxRuntime Objektverweis zu halten, und gibt einen Proxy es
  • The TimeboxRuntime Objekt in der Kind AppDomain nimmt eine Action als Eingabe in der untergeordneten Domäne ausgeführt
  • Timebox erstellt dann eine Aufgabe zum Aufruf der TimeboxRuntime prox y
  • Die Aufgabe (und die Aktionsausführung beginnt) und der „main“ Thread wartet so lange, wie die gegebenen TimeSpan
  • Nachdem die TimeSpan gegebenen gestartet wird (oder wenn die Aufgabe abgeschlossen ist), das Kind AppDomain ist entladen, ob die Action abgeschlossen wurde oder nicht.
  • A TimeoutException wird ausgelöst, wenn action mal aus, sonst wenn action eine Ausnahme auslöst, wird es durch das Kind gefangen AppDomain und kehrte für die AppDomain Aufruf

Ein Nachteil zu werfen ist, dass Ihr Programm erhöhten benötigen genug Berechtigungen, um eine AppDomain zu erstellen.

Hier ist ein Beispielprogramm, das zeigt, wie es funktioniert (ich glaube, Sie können dies kopieren, wenn Sie die richtigen using s). Ich habe auch erstellt, wenn Sie interessiert sind.

public class Program 
{ 
    public static void Main() 
    { 
     try 
     { 
      var timebox = new Timebox(TimeSpan.FromSeconds(1)); 
      timebox.Execute(() => 
      { 
       // do your thing 
       for (var i = 0; i < 1000; i++) 
       { 
        Console.WriteLine(i); 
       } 
      }); 

      Console.WriteLine("Didn't Time Out"); 
     } 
     catch (TimeoutException e) 
     { 
      Console.WriteLine("Timed Out"); 
      // handle it 
     } 
     catch(Exception e) 
     { 
      Console.WriteLine("Another exception was thrown in your timeboxed function"); 
      // handle it 
     } 
     Console.WriteLine("Program Finished"); 
     Console.ReadLine(); 
    } 
} 

public class Timebox 
{ 
    private readonly TimeSpan _ts; 

    public Timebox(TimeSpan ts) 
    { 
     _ts = ts; 
    } 

    public void Execute(Action func) 
    { 
     AppDomain childDomain = null; 
     try 
     { 
      // Construct and initialize settings for a second AppDomain. Perhaps some of 
      // this is unnecessary but perhaps not. 
      var domainSetup = new AppDomainSetup() 
      { 
       ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase, 
       ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, 
       ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName, 
       LoaderOptimization = LoaderOptimization.MultiDomainHost 
      }; 

      // Create the child AppDomain 
      childDomain = AppDomain.CreateDomain("Timebox Domain", null, domainSetup); 

      // Create an instance of the timebox runtime child AppDomain 
      var timeboxRuntime = (ITimeboxRuntime)childDomain.CreateInstanceAndUnwrap(
       typeof(TimeboxRuntime).Assembly.FullName, typeof(TimeboxRuntime).FullName); 

      // Start the runtime, by passing it the function we're timboxing 
      Exception ex = null; 
      var timeoutOccurred = true; 
      var task = new Task(() => 
      { 
       ex = timeboxRuntime.Run(func); 
       timeoutOccurred = false; 
      }); 

      // start task, and wait for the alloted timespan. If the method doesn't finish 
      // by then, then we kill the childDomain and throw a TimeoutException 
      task.Start(); 
      task.Wait(_ts); 

      // if the timeout occurred then we throw the exception for the caller to handle. 
      if(timeoutOccurred) 
      { 
       throw new TimeoutException("The child domain timed out"); 
      } 

      // If no timeout occurred, then throw whatever exception was thrown 
      // by our child AppDomain, so that calling code "sees" the exception 
      // thrown by the code that it passes in. 
      if(ex != null) 
      { 
       throw ex; 
      } 
     } 
     finally 
     { 
      // kill the child domain whether or not the function has completed 
      if(childDomain != null) AppDomain.Unload(childDomain); 
     } 
    } 

    // don't strictly need this, but I prefer having an interface point to the proxy 
    private interface ITimeboxRuntime 
    { 
     Exception Run(Action action); 
    } 

    // Need to derive from MarshalByRefObject... proxy is returned across AppDomain boundary. 
    private class TimeboxRuntime : MarshalByRefObject, ITimeboxRuntime 
    { 
     public Exception Run(Action action) 
     { 
      try 
      { 
       // Nike: just do it! 
       action(); 
      } 
      catch(Exception e) 
      { 
       // return the exception to be thrown in the calling AppDomain 
       return e; 
      } 
      return null; 
     } 
    } 
} 

EDIT:

Der Grund, warum ich mit einem AppDomain statt Thread s oder Task s nur ging, ist, weil es keine kugelsichere Art und Weise Thread s oder Task s für beliebigen Code zum Beenden [1] [2] [3]. Eine AppDomain für Ihre Anforderungen schien mir der beste Ansatz zu sein.

+0

Obwohl ich nicht immer erhöhte Berechtigungen verwenden kann, ist dies eine sehr saubere Lösung. Ich werde das wahrscheinlich in Stücken benutzen. – Jerry

+0

Wenn 'Thread.Abort()' etwas ist, das Sie gerne benutzen, dann funktioniert das genauso wie mit einer 'AppDomain'. Anstatt 'ex = TimeboxRuntime.Run (func)' können Sie den Thread (wie in Ihrer veröffentlichten Lösung) erfassen und 'func()' in der Task aufrufen. Später, anstatt die Domain zu entladen, können Sie den Thread abbrechen (wenn er noch läuft). Dies hätte keine erhöhten Berechtigungen erforderlich. ** Ich würde immer noch empfehlen, die 'AppDomain' ** zu verwenden, da ich immer dem Rat von Eric Lippert folge (http://stackoverflow.com/questions/1559255/whats-wrong-with-using-thread-abort/1560567) # 1560567) :). –

2

Hier ist ein Asynchron-Implementierung von Timeouts:

... 
     private readonly semaphore = new SemaphoreSlim(1,1); 

    ... 
     // total time allowed here is 100ms 
     var tokenSource = new CancellationTokenSource(100); 
     try{ 
     await WorkMethod(parameters, tokenSource.Token); // work 
     } catch (OperationCancelledException ocx){ 
     // gracefully handle cancellations: 
     label.Text = "Operation timed out"; 
     } 
    ... 

    public async Task WorkMethod(object prm, CancellationToken ct){ 
     try{ 
     await sem.WaitAsync(ct); // equivalent to lock(object){...} 
     // synchronized work, 
     // call tokenSource.Token.ThrowIfCancellationRequested() or 
     // check tokenSource.IsCancellationRequested in long-running blocks 
     // and pass ct to other tasks, such as async HTTP or stream operations 
     } finally { 
     sem.Release(); 
     } 
    } 

nicht, dass ich es empfehlen, aber Sie die tokenSource statt seiner Token in WorkMethod und tokenSource.CancelAfter(200) regelmäßig tun passieren könnte mehr Zeit hinzuzufügen, wenn Sie Sicher, du bist nicht an einem Ort, der totgesperrt sein kann (Warte auf einen HTTP-Anruf), aber ich denke, das wäre ein esoterischer Ansatz für Multithreading.

Stattdessen sollten Ihre Threads so schnell wie möglich sein (Minimum IO) und ein Thread kann die Ressourcen serialisieren (Produzent), während andere eine Warteschlange (Consumer) verarbeiten, wenn Sie E/A Multithreading (z. B. Dateikomprimierung, Downloads usw.)) und vermeiden Sie die Deadlock-Möglichkeit.

0

Ich mochte die visuelle Idee einer Verwendung Aussage. Jedoch, das ist keine praktikable Lösung. Warum? Nun, ein Unter-Thread (das Objekt/thread/timer innerhalb der using-Anweisung) kann den Haupt-Thread nicht unterbrechen und eine Ausnahme einfügen, was dazu führt, dass er aufhört, was er tut, und zum nächsten try/catch springt. Darauf kommt es an. Je mehr ich saß und damit arbeitete, desto mehr kam ans Licht.

Kurz gesagt, es kann nicht so gemacht werden, wie ich es wollte.

Allerdings habe ich Pieter Ansatz genommen und mein Code ein wenig gemangelt. Es führt zu einigen Lesbarkeitsproblemen, aber ich habe versucht, sie mit Kommentaren und so zu mildern.

public void MyMethod(...) 
{ 

... 

    // Placeholder for thread to kill if the action times out. 
    Thread threadToKill = null; 
    Action wrappedAction =() => 
    { 
     // Take note of the action's thread. We may need to kill it later. 
     threadToKill = Thread.CurrentThread; 

     ... 
     /* DO STUFF HERE */ 
     ... 

    }; 

    // Now, execute the action. We'll deal with the action timeouts below. 
    IAsyncResult result = wrappedAction.BeginInvoke(null, null); 

    // Set the timeout to 10 minutes. 
    if (result.AsyncWaitHandle.WaitOne(10 * 60 * 1000)) 
    { 
     // Everything was successful. Just clean up the invoke and get out. 
     wrappedAction.EndInvoke(result); 
    } 
    else 
    { 
     // We have timed out. We need to abort the thread!! 
     // Don't let it continue to try to do work. Something may be stuck. 
     threadToKill.Abort(); 
     throw new TimeoutException("This code block timed out"); 
    } 

... 
} 

Da ich dies an drei oder vier Stellen pro Hauptabschnitt mache, wird dies schwieriger zu lesen. Es funktioniert jedoch ganz gut.

+0

Es ist nicht notwendig, all diesen Code zu duplizieren - Sie können ihn in eine Hilfsmethode einfügen, die ein 'Action' als Argument verwendet, und diese' Aktion' innerhalb von 'wrappedAction' aufrufen. Beachten Sie, dass Ihr Code den aufrufenden Thread blockiert und dass es empfohlen wird, Code zu schreiben, der mit dem Abbruch zusammenarbeitet, anstatt einen Thread "gewaltsam" abzubrechen. Der moderne Ansatz wäre, eine "Task" zusammen mit einem "CancellationToken (Source)" zu verwenden (was zweckmäßigerweise eine Zeitüberschreitung als Argument annehmen kann). –

+1

@Jerry Eine Anmerkung: 'Thread.Abort()' ist umstritten. [Laut Eric Lippert] (http://stackoverflow.com/a/1560567/3960399), der die Autorität auf C# ist, sollte dies um jeden Preis vermieden werden. –

Verwandte Themen