Zaimplementuj ogólny limit czasu C #

157

Szukam dobrych pomysłów na implementację ogólnego sposobu wykonywania pojedynczego wiersza (lub anonimowego delegata) kodu z przekroczeniem limitu czasu.

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

Szukam rozwiązania, które będzie można elegancko zaimplementować w wielu miejscach, w których mój kod współdziała z kodem temperamentu (którego nie mogę zmienić).

Ponadto chciałbym, aby naruszający kod „przekroczony limit czasu” przestał być wykonywany dalej, jeśli to możliwe.

chilltemp
źródło
46
Przypomnienie dla wszystkich, którzy przyjrzą się poniższym odpowiedziom: Wielu z nich używa Thread.Abort, co może być bardzo złe. Przeczytaj różne komentarze na ten temat przed zaimplementowaniem Abort w swoim kodzie. Czasami może to być odpowiednie, ale są one rzadkie. Jeśli nie rozumiesz dokładnie, co robi Abort lub nie jest to potrzebne, zaimplementuj jedno z poniższych rozwiązań, które go nie używają. Są to rozwiązania, na które nie ma tylu głosów, ponieważ nie odpowiadały potrzebom mojego pytania.
chilltemp
Dzięki za poradę. +1 głos.
QueueHammer
7
Aby uzyskać szczegółowe informacje na temat zagrożeń związanych z nitkami, przeczytaj artykuł Erica Lipperta: blogs.msdn.com/b/ericlippert/archive/2010/02/22/…
JohnW

Odpowiedzi:

95

Naprawdę trudną częścią było zabicie długotrwałego zadania poprzez przekazanie wątku wykonawcy z akcji z powrotem do miejsca, w którym można go przerwać. Dokonałem tego za pomocą opakowanego delegata, który przekazuje wątek do zabicia do zmiennej lokalnej w metodzie, która utworzyła lambdę.

Podaję ten przykład dla twojej przyjemności. Metoda, którą naprawdę jesteś zainteresowany, to CallWithTimeout. Spowoduje to anulowanie długotrwałego wątku przez przerwanie go i połknięcie wyjątku ThreadAbortException :

Stosowanie:

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);
    }

Metoda statyczna wykonująca pracę:

    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
źródło
3
Dlaczego haczyk (ThreadAbortException)? AFAIK, nie możesz naprawdę złapać wyjątku ThreadAbortException (zostanie on ponownie wyrzucony po opuszczeniu bloku catch).
csgero
12
Thread.Abort () jest bardzo niebezpieczny w użyciu, nie powinien być używany ze zwykłym kodem, tylko kod, który gwarantuje bezpieczeństwo, powinien zostać przerwany, taki jak kod Cer.Safe, używa ograniczonych regionów wykonywania i bezpiecznych uchwytów. Nie powinno się tego robić dla żadnego kodu.
Pop Catalin,
12
Chociaż Thread.Abort () jest zły, nie jest tak zły, jak proces wymykający się spod kontroli i wykorzystujący każdy cykl procesora i bajt pamięci, który ma komputer. Masz jednak rację, wskazując potencjalne problemy każdemu, kto może uważać ten kod za przydatny.
chilltemp
24
Nie mogę uwierzyć, że to jest zaakceptowana odpowiedź, ktoś nie może czytać tutaj komentarzy lub odpowiedź została zaakceptowana przed komentarzami i ta osoba nie sprawdza swojej strony z odpowiedziami. Wątek Abort nie jest rozwiązaniem, to tylko kolejny problem, który musisz rozwiązać!
Lasse V. Karlsen
18
To Ty nie czytasz komentarzy. Jak mówi chilltemp powyżej, wywołuje kod, nad którym NIE ma kontroli - i chce, aby został przerwany. Nie ma innej opcji niż Thread.Abort (), jeśli chce, aby to działało w jego procesie. Masz rację, że Thread.Abort jest zły - ale jak mówi chilltemp, inne rzeczy są gorsze!
TheSoftwareJedi
73

W produkcji intensywnie używamy takiego kodu :

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

Wdrożenie ma charakter open source, działa wydajnie nawet w równoległych scenariuszach obliczeniowych i jest dostępne w ramach współdzielonych bibliotek Lokad

/// <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);
    }
}

Ten kod nadal zawiera błędy, możesz spróbować za pomocą tego małego programu testowego:

      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());
      }
   }

Jest stan wyścigu. Jest oczywiście możliwe, że wyjątek ThreadAbortException zostanie zgłoszony po WaitFor<int>.Run()wywołaniu metody . Nie znalazłem niezawodnego sposobu, aby to naprawić, jednak za pomocą tego samego testu nie mogę powtórzyć żadnego problemu z zaakceptowaną odpowiedzią TheSoftwareJedi .

wprowadź opis obrazu tutaj

Rinat Abdullin
źródło
3
To co zaimplementowałem, może obsłużyć parametry i zwrócić wartość, które preferuję i potrzebuję. Dzięki Rinat
Gabriel Mongeon
7
co to jest [Niezmienne]?
raklos
2
Tylko atrybutem używamy do oznaczenia klas niezmienne (niezmienność jest weryfikowana przez Mono Cecil w testach jednostkowych)
Rinat Abdullin
9
To jest impas, który ma się wydarzyć (jestem zaskoczony, że jeszcze go nie zauważyłeś). Twoje wywołanie watchedThread.Abort () znajduje się wewnątrz zamka, który również należy zdobyć w ostatnim bloku. Oznacza to, że podczas gdy ostatni blok czeka na blokadę (ponieważ obserwowany wątek ma go między powrotem Wait () a Thread.Abort ()), wywołanie watchedThread.Abort () również będzie blokować w nieskończoność, czekając na ostateczne zakończenie (co nigdy nie będzie). Therad.Abort () może blokować, jeśli działa chroniony region kodu - powodując zakleszczenia, patrz - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev
1
trickdev, wielkie dzięki. Z jakiegoś powodu występowanie impasu wydaje się być bardzo rzadkie, ale mimo to naprawiliśmy kod :-)
Joannes Vermorel
15

Cóż, możesz robić rzeczy z delegatami (BeginInvoke, z wywołaniem zwrotnym ustawiającym flagę - i oryginalny kod czekający na tę flagę lub limit czasu) - ale problem polega na tym, że bardzo trudno jest zamknąć działający kod. Na przykład zabijanie (lub wstrzymywanie) wątku jest niebezpieczne ... więc nie sądzę, aby można było to łatwo zrobić solidnie.

Opublikuję to, ale zauważ, że nie jest to idealne rozwiązanie - nie zatrzymuje długotrwałego zadania i nie czyści prawidłowo po niepowodzeniu.

    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
źródło
2
Jestem całkowicie szczęśliwy, zabijając coś, co mnie zraniło. To wciąż lepsze niż pozwolenie mu zjadać cykle procesora do następnego ponownego uruchomienia (jest to część usługi systemu Windows).
chilltemp
@Marc: Jestem twoim wielkim fanem. Ale tym razem zastanawiam się, dlaczego nie użyłeś result.AsyncWaitHandle, o którym wspomina TheSoftwareJedi. Jakieś korzyści z używania ManualResetEvent zamiast AsyncWaitHandle?
Anand Patel
1
@ No cóż, to było kilka lat temu, więc nie mogę odpowiedzieć z pamięci - ale „łatwe do zrozumienia” liczy się bardzo w przypadku wątków kodu
Marc Gravell
13

Kilka drobnych zmian w świetnej odpowiedzi Pop Catalina:

  • Func zamiast Action
  • Zgłoś wyjątek w przypadku złej wartości limitu czasu
  • Wywołanie EndInvoke w przypadku przekroczenia limitu czasu

Dodano przeciążenia w celu obsługi sygnalizowania procesu roboczego w celu anulowania wykonania:

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
źródło
Invoke (e => {// ... if (błąd) e.Cancel = true; return 5;}, TimeSpan.FromSeconds (5));
George Tsiokos
1
Warto zauważyć, że w tej odpowiedzi metoda „przekroczony limit czasu” pozostaje uruchomiona, chyba że można ją zmodyfikować, aby grzecznie wybrać opcję zakończenia, gdy zostanie oznaczona jako „anuluj”.
David Eison,
Davidzie, właśnie do tego celu stworzono typ CancellationToken (.NET 4.0). W tej odpowiedzi użyłem CancelEventArgs, aby pracownik mógł sondować argumenty Anuluj, aby sprawdzić, czy powinien zakończyć, chociaż należy to ponownie zaimplementować za pomocą CancellationToken dla .NET 4.0.
George Tsiokos
Uwaga dotycząca użytkowania, która na chwilę mnie zdezorientowała: potrzebujesz dwóch bloków try / catch, jeśli kod funkcji / akcji może zgłosić wyjątek po przekroczeniu limitu czasu. Potrzebujesz jednej próby / złapania wokół wywołania Invoke, aby złapać TimeoutException. Potrzebujesz sekundy w swojej funkcji / akcji, aby przechwycić i połknąć / zarejestrować każdy wyjątek, który może wystąpić po rzutach limitu czasu. W przeciwnym razie aplikacja zakończy działanie z nieobsługiwanym wyjątkiem (moim przypadkiem użycia jest ping testowanie połączenia WCF przy mniejszym limicie czasu niż określony w app.config)
fiat
Absolutnie - ponieważ kod wewnątrz funkcji / akcji może rzucać, musi znajdować się wewnątrz try / catch. Zgodnie z konwencją te metody nie próbują próbować / przechwytywać funkcji / akcji. Wyłapywanie i wyrzucanie wyjątku to zły projekt. Podobnie jak w przypadku każdego kodu asynchronicznego, użytkownik metody spróbuje / złapie.
George Tsiokos,
10

Oto jak bym to zrobił:

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
źródło
3
to nie zatrzymuje wykonywania zadania
TheSoftwareJedi
2
Nie wszystkie zadania można bezpiecznie zatrzymać, mogą pojawić się różnego rodzaju problemy, zakleszczenia, wycieki zasobów, uszkodzenie stanu… Nie należy tego robić w ogólnym przypadku.
Pop Catalin,
7

Właśnie to znokautowałem, więc może wymagać poprawy, ale zrobi, co chcesz. Jest to prosta aplikacja na konsolę, ale przedstawia niezbędne zasady.

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
źródło
1
Miły. Jedyne, co chciałbym dodać, to to, że może wolałby rzucić System.TimeoutException niż tylko System.Exception
Joel Coehoorn
Och, tak: i zawarłbym to również w swojej własnej klasie.
Joel Coehoorn
2

A co z użyciem 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");
}

źródło
1
To powiadomi metodę wywołującą o problemie, ale nie spowoduje przerwania wątku powodującego problem.
chilltemp
1
Nie jestem pewien, czy to prawda. Z dokumentacji nie wynika jasno, co dzieje się z wątkiem roboczym po upłynięciu limitu czasu połączenia.
Matthew Lowe