Implementieren Sie das generische C # -Zeitlimit

157

Ich suche nach guten Ideen für die Implementierung einer generischen Methode, um eine einzelne Zeile (oder einen anonymen Delegaten) von Code mit einer Zeitüberschreitung auszuführen.

TemperamentalClass tc = new TemperamentalClass();
tc.DoSomething();  // normally runs in 30 sec.  Want to error at 1 min

Ich suche nach einer Lösung, die an vielen Stellen elegant implementiert werden kann, an denen mein Code mit temperamentvollem Code interagiert (den ich nicht ändern kann).

Außerdem möchte ich, dass der fehlerhafte "Timeout" -Code nach Möglichkeit nicht mehr weiter ausgeführt wird.

chilltemp
quelle
46
Nur eine Erinnerung an alle, die sich die folgenden Antworten ansehen: Viele von ihnen verwenden Thread.Abort, was sehr schlecht sein kann. Bitte lesen Sie die verschiedenen Kommentare dazu, bevor Sie Abort in Ihren Code implementieren. Es kann gelegentlich angebracht sein, aber diese sind selten. Wenn Sie nicht genau verstehen, was Abort tut oder nicht benötigt, implementieren Sie bitte eine der folgenden Lösungen, die es nicht verwenden. Sie sind die Lösungen, die nicht so viele Stimmen haben, weil sie nicht den Anforderungen meiner Frage entsprachen.
Chilltemp
Danke für den Hinweis. +1 Stimme.
QueueHammer
7
Für Details zu den Gefahren von thread.Abort lesen Sie diesen Artikel von Eric Lippert: blogs.msdn.com/b/ericlippert/archive/2010/02/22/…
JohnW

Antworten:

95

Der wirklich schwierige Teil hier war, die lange laufende Aufgabe zu beenden, indem der Executor-Thread von der Aktion an einen Ort zurückgeleitet wurde, an dem er abgebrochen werden konnte. Ich habe dies mit der Verwendung eines umschlossenen Delegaten erreicht, der den Thread zum Töten in eine lokale Variable in der Methode übergibt, mit der das Lambda erstellt wurde.

Ich reiche dieses Beispiel zu Ihrem Vergnügen ein. Die Methode, an der Sie wirklich interessiert sind, ist CallWithTimeout. Dadurch wird der lange laufende Thread abgebrochen, indem er abgebrochen und die ThreadAbortException verschluckt wird :

Verwendung:

class Program
{

    static void Main(string[] args)
    {
        //try the five second method with a 6 second timeout
        CallWithTimeout(FiveSecondMethod, 6000);

        //try the five second method with a 4 second timeout
        //this will throw a timeout exception
        CallWithTimeout(FiveSecondMethod, 4000);
    }

    static void FiveSecondMethod()
    {
        Thread.Sleep(5000);
    }

Die statische Methode, die die Arbeit erledigt:

    static void CallWithTimeout(Action action, int timeoutMilliseconds)
    {
        Thread threadToKill = null;
        Action wrappedAction = () =>
        {
            threadToKill = Thread.CurrentThread;
            try
            {
                action();
            }
            catch(ThreadAbortException ex){
               Thread.ResetAbort();// cancel hard aborting, lets to finish it nicely.
            }
        };

        IAsyncResult result = wrappedAction.BeginInvoke(null, null);
        if (result.AsyncWaitHandle.WaitOne(timeoutMilliseconds))
        {
            wrappedAction.EndInvoke(result);
        }
        else
        {
            threadToKill.Abort();
            throw new TimeoutException();
        }
    }

}
TheSoftwareJedi
quelle
3
Warum der Fang (ThreadAbortException)? AFAIK Sie können eine ThreadAbortException nicht wirklich abfangen (sie wird nach dem Verlassen des catch-Blocks erneut ausgelöst).
Csgero
12
Thread.Abort () ist sehr gefährlich zu verwenden. Es sollte nicht mit normalem Code verwendet werden. Es sollte nur Code abgebrochen werden, der garantiert sicher ist, z. B. Cer.Safe-Code, der eingeschränkte Ausführungsbereiche und sichere Handles verwendet. Es sollte für keinen Code gemacht werden.
Pop Catalin
12
Während Thread.Abort () schlecht ist, ist es nicht annähernd so schlecht wie ein Prozess, der außer Kontrolle gerät und jeden CPU-Zyklus und jedes Byte Speicher des PCs verwendet. Sie können jedoch zu Recht alle anderen auf die potenziellen Probleme hinweisen, die diesen Code für nützlich halten.
Chilltemp
24
Ich kann nicht glauben, dass dies die akzeptierte Antwort ist, jemand darf die Kommentare hier nicht lesen, oder die Antwort wurde vor den Kommentaren akzeptiert und diese Person überprüft ihre Antwortseite nicht. Thread.Abort ist keine Lösung, es ist nur ein weiteres Problem, das Sie lösen müssen!
Lasse V. Karlsen
18
Sie sind derjenige, der die Kommentare nicht liest. Wie chilltemp oben sagt, ruft er Code auf, über den er KEINE Kontrolle hat - und möchte, dass er abgebrochen wird. Er hat keine andere Option als Thread.Abort (), wenn er möchte, dass dies in seinem Prozess ausgeführt wird. Sie haben Recht, dass Thread.Abort schlecht ist - aber wie Chilltemp sagt, sind andere Dinge schlimmer!
TheSoftwareJedi
73

Wir verwenden Code wie diesen häufig in der Produktion :

var result = WaitFor<Result>.Run(1.Minutes(), () => service.GetSomeFragileResult());

Die Implementierung erfolgt über Open Source, funktioniert auch in parallelen Computerszenarien effizient und ist als Teil von Lokad Shared Libraries verfügbar

/// <summary>
/// Helper class for invoking tasks with timeout. Overhead is 0,005 ms.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
[Immutable]
public sealed class WaitFor<TResult>
{
    readonly TimeSpan _timeout;

    /// <summary>
    /// Initializes a new instance of the <see cref="WaitFor{T}"/> class, 
    /// using the specified timeout for all operations.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    public WaitFor(TimeSpan timeout)
    {
        _timeout = timeout;
    }

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval. 
    /// </summary>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public TResult Run(Func<TResult> function)
    {
        if (function == null) throw new ArgumentNullException("function");

        var sync = new object();
        var isCompleted = false;

        WaitCallback watcher = obj =>
            {
                var watchedThread = obj as Thread;

                lock (sync)
                {
                    if (!isCompleted)
                    {
                        Monitor.Wait(sync, _timeout);
                    }
                }
                   // CAUTION: the call to Abort() can be blocking in rare situations
                    // http://msdn.microsoft.com/en-us/library/ty8d3wta.aspx
                    // Hence, it should not be called with the 'lock' as it could deadlock
                    // with the 'finally' block below.

                    if (!isCompleted)
                    {
                        watchedThread.Abort();
                    }
        };

        try
        {
            ThreadPool.QueueUserWorkItem(watcher, Thread.CurrentThread);
            return function();
        }
        catch (ThreadAbortException)
        {
            // This is our own exception.
            Thread.ResetAbort();

            throw new TimeoutException(string.Format("The operation has timed out after {0}.", _timeout));
        }
        finally
        {
            lock (sync)
            {
                isCompleted = true;
                Monitor.Pulse(sync);
            }
        }
    }

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public static TResult Run(TimeSpan timeout, Func<TResult> function)
    {
        return new WaitFor<TResult>(timeout).Run(function);
    }
}

Dieser Code ist immer noch fehlerhaft. Sie können ihn mit diesem kleinen Testprogramm ausprobieren:

      static void Main(string[] args) {

         // Use a sb instead of Console.WriteLine() that is modifying how synchronous object are working
         var sb = new StringBuilder();

         for (var j = 1; j < 10; j++) // do the experiment 10 times to have chances to see the ThreadAbortException
         for (var ii = 8; ii < 15; ii++) {
            int i = ii;
            try {

               Debug.WriteLine(i);
               try {
                  WaitFor<int>.Run(TimeSpan.FromMilliseconds(10), () => {
                     Thread.Sleep(i);
                     sb.Append("Processed " + i + "\r\n");
                     return i;
                  });
               }
               catch (TimeoutException) {
                  sb.Append("Time out for " + i + "\r\n");
               }

               Thread.Sleep(10);  // Here to wait until we get the abort procedure
            }
            catch (ThreadAbortException) {
               Thread.ResetAbort();
               sb.Append(" *** ThreadAbortException on " + i + " *** \r\n");
            }
         }

         Console.WriteLine(sb.ToString());
      }
   }

Es gibt eine Rennbedingung. Es ist eindeutig möglich, dass eine ThreadAbortException ausgelöst wird, nachdem die Methode WaitFor<int>.Run()aufgerufen wurde. Ich habe keinen zuverlässigen Weg gefunden, um dies zu beheben, aber mit demselben Test kann ich kein Problem mit der von TheSoftwareJedi akzeptierten Antwort wiederholen .

Geben Sie hier die Bildbeschreibung ein

Rinat Abdullin
quelle
3
Dies ist, was ich implementiert habe. Es kann Parameter und Rückgabewerte verarbeiten, die ich bevorzuge und benötige. Vielen Dank Rinat
Gabriel Mongeon
7
Was ist [unveränderlich]?
Raklos
2
Nur ein Attribut, das wir verwenden, um unveränderliche Klassen zu markieren (Unveränderlichkeit wird von Mono Cecil in Unit-Tests überprüft)
Rinat Abdullin
9
Dies ist ein Deadlock, der darauf wartet, passiert zu werden (ich bin überrascht, dass Sie ihn noch nicht beobachtet haben). Ihr Aufruf von watchThread.Abort () befindet sich in einem Schloss, das auch im finally-Block erfasst werden muss. Dies bedeutet, während der finally-Block auf die Sperre wartet (da der watchThread sie zwischen Wait () und Thread.Abort () hat), blockiert der watchThread.Abort () -Aufruf auch das unbegrenzte Warten auf das endgültige Ende (was es ist) wird es nie). Therad.Abort () kann blockieren, wenn ein geschützter Codebereich ausgeführt wird - was zu Deadlocks führt, siehe - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev
1
trickdev, vielen dank. Aus irgendeinem Grund scheint das Auftreten von Deadlocks sehr selten zu sein, aber wir haben den Code trotzdem behoben :-)
Joannes Vermorel
15

Nun, Sie könnten Dinge mit Delegaten tun (BeginInvoke, mit einem Rückruf, der ein Flag setzt - und dem ursprünglichen Code, der auf dieses Flag oder Timeout wartet) -, aber das Problem ist, dass es sehr schwierig ist, den laufenden Code herunterzufahren. Zum Beispiel ist das Töten (oder Anhalten) eines Threads gefährlich ... daher glaube ich nicht, dass es einen einfachen Weg gibt, dies robust zu tun.

Ich werde dies posten, aber beachten Sie, dass es nicht ideal ist - es stoppt die lang laufende Aufgabe nicht und bereinigt bei einem Fehler nicht richtig.

    static void Main()
    {
        DoWork(OK, 5000);
        DoWork(Nasty, 5000);
    }
    static void OK()
    {
        Thread.Sleep(1000);
    }
    static void Nasty()
    {
        Thread.Sleep(10000);
    }
    static void DoWork(Action action, int timeout)
    {
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate {evt.Set();};
        IAsyncResult result = action.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        {
            action.EndInvoke(result);
        }
        else
        {
            throw new TimeoutException();
        }
    }
    static T DoWork<T>(Func<T> func, int timeout)
    {
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate { evt.Set(); };
        IAsyncResult result = func.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        {
            return func.EndInvoke(result);
        }
        else
        {
            throw new TimeoutException();
        }
    }
Marc Gravell
quelle
2
Ich bin vollkommen glücklich, etwas zu töten, das mich verrückt gemacht hat. Es ist immer noch besser, als es CPU-Zyklen bis zum nächsten Neustart essen zu lassen (dies ist Teil eines Windows-Dienstes).
Chilltemp
@Marc: Ich bin ein großer Fan von dir. Aber diesmal frage ich mich, warum Sie das Ergebnis nicht verwendet haben. AsyncWaitHandle, wie von TheSoftwareJedi erwähnt. Gibt es Vorteile bei der Verwendung von ManualResetEvent gegenüber AsyncWaitHandle?
Anand Patel
1
@ Und nun, das war vor einigen Jahren, also kann ich nicht aus dem Gedächtnis antworten - aber "leicht zu verstehen" zählt viel für Thread-Code
Marc Gravell
13

Einige kleinere Änderungen an Pop Catalins großartiger Antwort:

  • Func statt Action
  • Ausnahme bei schlechtem Timeout-Wert auslösen
  • Aufruf von EndInvoke im Falle einer Zeitüberschreitung

Überladungen wurden hinzugefügt, um den Signalisierungs-Worker beim Abbrechen der Ausführung zu unterstützen:

public static T Invoke<T> (Func<CancelEventArgs, T> function, TimeSpan timeout) {
    if (timeout.TotalMilliseconds <= 0)
        throw new ArgumentOutOfRangeException ("timeout");

    CancelEventArgs args = new CancelEventArgs (false);
    IAsyncResult functionResult = function.BeginInvoke (args, null, null);
    WaitHandle waitHandle = functionResult.AsyncWaitHandle;
    if (!waitHandle.WaitOne (timeout)) {
        args.Cancel = true; // flag to worker that it should cancel!
        /* •————————————————————————————————————————————————————————————————————————•
           | IMPORTANT: Always call EndInvoke to complete your asynchronous call.   |
           | http://msdn.microsoft.com/en-us/library/2e08f6yc(VS.80).aspx           |
           | (even though we arn't interested in the result)                        |
           •————————————————————————————————————————————————————————————————————————• */
        ThreadPool.UnsafeRegisterWaitForSingleObject (waitHandle,
            (state, timedOut) => function.EndInvoke (functionResult),
            null, -1, true);
        throw new TimeoutException ();
    }
    else
        return function.EndInvoke (functionResult);
}

public static T Invoke<T> (Func<T> function, TimeSpan timeout) {
    return Invoke (args => function (), timeout); // ignore CancelEventArgs
}

public static void Invoke (Action<CancelEventArgs> action, TimeSpan timeout) {
    Invoke<int> (args => { // pass a function that returns 0 & ignore result
        action (args);
        return 0;
    }, timeout);
}

public static void TryInvoke (Action action, TimeSpan timeout) {
    Invoke (args => action (), timeout); // ignore CancelEventArgs
}
George Tsiokos
quelle
Invoke (e => {// ... if (Fehler) e.Cancel = true; return 5;}, TimeSpan.FromSeconds (5));
George Tsiokos
1
Es ist darauf hinzuweisen, dass in dieser Antwort die Methode "Zeitüberschreitung" weiterhin ausgeführt wird, es sei denn, sie kann so geändert werden, dass sie höflich beendet wird, wenn sie mit "Abbrechen" gekennzeichnet ist.
David Eison
David, genau dafür wurde der CancellationToken-Typ (.NET 4.0) speziell entwickelt. In dieser Antwort habe ich CancelEventArgs verwendet, damit der Worker args.Cancel abfragen kann, um zu sehen, ob es beendet werden soll, obwohl dies mit dem CancellationToken für .NET 4.0 erneut implementiert werden sollte.
George Tsiokos
Ein Verwendungshinweis dazu, der mich eine Weile verwirrt hat: Sie benötigen zwei Try / Catch-Blöcke, wenn Ihr Funktions- / Aktionscode nach einer Zeitüberschreitung eine Ausnahme auslösen kann. Sie benötigen einen Versuch / Fang um den Aufruf von Invoke, um TimeoutException abzufangen. Sie benötigen eine Sekunde in Ihrer Funktion / Aktion, um Ausnahmen zu erfassen und zu verschlucken / zu protokollieren, die nach dem Timeout auftreten können. Andernfalls wird die App mit einer nicht behandelten Ausnahme beendet (mein Anwendungsfall ist das Ping-Testen einer WCF-Verbindung mit einem kürzeren Zeitlimit als in app.config angegeben)
Fiat
Absolut - da der Code in der Funktion / Aktion ausgelöst werden kann, muss er sich in einem Versuch / Fang befinden. Gemäß der Konvention versuchen diese Methoden nicht, die Funktion / Aktion zu versuchen / abzufangen. Es ist ein schlechtes Design, die Ausnahme zu fangen und wegzuwerfen. Wie bei jedem asynchronen Code ist es Sache des Benutzers der Methode, zu versuchen / zu fangen.
George Tsiokos
10

So würde ich es machen:

public static class Runner
{
    public static void Run(Action action, TimeSpan timeout)
    {
        IAsyncResult ar = action.BeginInvoke(null, null);
        if (ar.AsyncWaitHandle.WaitOne(timeout))
            action.EndInvoke(ar); // This is necesary so that any exceptions thrown by action delegate is rethrown on completion
        else
            throw new TimeoutException("Action failed to complete using the given timeout!");
    }
}
Pop Catalin
quelle
3
Dies stoppt nicht die ausführende Aufgabe
TheSoftwareJedi
2
Nicht alle Aufgaben sind sicher zu stoppen, es können alle Arten von Problemen auftreten, Deadlocks, Ressourcenlecks, Korruption des Staates ... Dies sollte im allgemeinen Fall nicht durchgeführt werden.
Pop Catalin
7

Ich habe das gerade ausgeknockt, damit es vielleicht etwas verbessert werden muss, aber ich werde tun, was Sie wollen. Es ist eine einfache Konsolen-App, zeigt jedoch die erforderlichen Prinzipien.

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


namespace TemporalThingy
{
    class Program
    {
        static void Main(string[] args)
        {
            Action action = () => Thread.Sleep(10000);
            DoSomething(action, 5000);
            Console.ReadKey();
        }

        static void DoSomething(Action action, int timeout)
        {
            EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
            AsyncCallback callback = ar => waitHandle.Set();
            action.BeginInvoke(callback, null);

            if (!waitHandle.WaitOne(timeout))
                throw new Exception("Failed to complete in the timeout specified.");
        }
    }

}
Jason Jackson
quelle
1
Nett. Das einzige, was ich hinzufügen möchte, ist, dass er vielleicht lieber System.TimeoutException als nur System.Exception auslösen möchte
Joel Coehoorn
Oh ja: und das würde ich auch in seine eigene Klasse einwickeln.
Joel Coehoorn
2

Was ist mit Thread.Join (int timeout)?

public static void CallWithTimeout(Action act, int millisecondsTimeout)
{
    var thread = new Thread(new ThreadStart(act));
    thread.Start();
    if (!thread.Join(millisecondsTimeout))
        throw new Exception("Timed out");
}

quelle
1
Dies würde die aufrufende Methode über ein Problem informieren, den fehlerhaften Thread jedoch nicht abbrechen.
Chilltemp
1
Ich bin mir nicht sicher, ob das richtig ist. Aus der Dokumentation geht nicht hervor, was mit dem Worker-Thread passiert, wenn das Join-Timeout abgelaufen ist.
Matthew Lowe