Implementare il timeout generico C #


157

Sto cercando buone idee per implementare un modo generico per far eseguire una sola riga (o un delegato anonimo) con un timeout.

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

Sto cercando una soluzione che possa essere elegantemente implementata in molti luoghi in cui il mio codice interagisce con il codice temperamentale (che non posso cambiare).

Inoltre, vorrei che il codice "timeout" offensivo venisse interrotto dall'esecuzione ulteriore, se possibile.


46
Solo un promemoria per chiunque guardi le risposte qui sotto: molti di loro usano Thread.Abort che può essere molto male. Si prega di leggere i vari commenti a riguardo prima di implementare Abort nel proprio codice. Può essere appropriato in alcune occasioni, ma quelli sono rari. Se non capisci esattamente cosa fa Abort o non ne hai bisogno, ti preghiamo di implementare una delle soluzioni di seguito che non lo utilizza. Sono le soluzioni che non hanno tanti voti perché non soddisfano le esigenze della mia domanda.
Chilltemp,

Grazie per la consulenza. +1 voto.
QueueHammer

7
Per dettagli sui pericoli del thread.Abort, leggi questo articolo di Eric Lippert: blogs.msdn.com/b/ericlippert/archive/2010/02/22/…
JohnW

Risposte:


95

La parte davvero difficile qui è stata uccidere l'attività di lunga durata passando il filo dell'esecutore dall'Azione in un posto dove poteva essere interrotto. Ho realizzato questo con l'uso di un delegato spostato che passa il thread per uccidere in una variabile locale nel metodo che ha creato la lambda.

Vi presento questo esempio, per il vostro divertimento. Il metodo che ti interessa veramente è CallWithTimeout. Ciò annullerà il thread con esecuzione prolungata interrompendolo e ingoiando ThreadAbortException :

Uso:

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

Il metodo statico che fa il lavoro:

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

}

3
Perché la cattura (ThreadAbortException)? AFAIK non puoi davvero catturare una ThreadAbortException (verrà riproposta dopo che il blocco catch è rimasto).
csgero,

12
Thread.Abort () è molto pericoloso da usare, non dovrebbe essere usato con codice normale, solo il codice che è garantito per essere sicuro dovrebbe essere interrotto, come il codice Cer.Safe, usa aree di esecuzione vincolate e handle sicuri. Non dovrebbe essere fatto per nessun codice.
Pop Catalin,

12
Sebbene Thread.Abort () sia dannoso, non è affatto male come un processo che va fuori controllo e utilizza ogni ciclo CPU e byte di memoria del PC. Ma hai ragione nel segnalare i potenziali problemi a chiunque possa ritenere utile questo codice.
Chilltemp,

24
Non posso credere che questa sia la risposta accettata, qualcuno non deve leggere i commenti qui o la risposta è stata accettata prima dei commenti e quella persona non controlla la sua pagina di risposte. Thread.Abort non è una soluzione, è solo un altro problema che devi risolvere!
Lasse V. Karlsen,

18
Sei tu quello che non legge i commenti. Come dice chilltemp sopra, sta chiamando il codice su cui NON ha alcun controllo e vuole che si interrompa. Non ha altra scelta che Thread.Abort () se vuole che questo avvenga all'interno del suo processo. Hai ragione che Thread.Abort è cattivo - ma come dice chilltemp, altre cose sono peggio!
TheSoftwareJedi,

73

Stiamo usando un codice come questo pesantemente in productio n:

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

L'implementazione è open source, funziona in modo efficiente anche in scenari di elaborazione paralleli ed è disponibile come parte delle librerie condivise di 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);
    }
}

Questo codice è ancora difettoso, puoi provare con questo piccolo programma di test:

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

C'è una condizione di gara. È chiaramente possibile che venga sollevata una ThreadAbortException dopo che il metodo WaitFor<int>.Run()è stato chiamato. Non ho trovato un modo affidabile per risolvere questo problema, tuttavia con lo stesso test non riesco a riproporre alcun problema con la risposta accettata da TheSoftwareJedi .

inserisci qui la descrizione dell'immagine


3
Questo è quello che ho implementato, può gestire i parametri e restituire valore, che preferisco e di cui ho bisogno. Grazie Rinat
Gabriel Mongeon,

7
che cos'è [Immutabile]?
raklos,

2
Solo un attributo che usiamo per contrassegnare le classi immutabili (l'immutabilità è verificata da Mono Cecil nei test unitari)
Rinat Abdullin,

9
Questo è un deadlock in attesa di accadere (sono sorpreso che non l'abbia ancora osservato). La tua chiamata a watchThread.Abort () è all'interno di un lucchetto, che deve anche essere acquisito nel blocco finally. Ciò significa che mentre il blocco finally è in attesa del blocco (poiché il watchThread lo ha tra Wait () return e Thread.Abort ()), anche la chiamata watchThread.Abort () bloccherà indefinitamente in attesa che il fine abbia fine (che mai). Therad.Abort () può bloccare se una regione protetta di codice viene eseguito - causando situazioni di stallo, vedi - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev

1
trickdev, grazie mille. Per qualche motivo, il verificarsi di deadlock sembra essere molto raro, ma abbiamo comunque corretto il codice :-)
Joannes Vermorel

15

Bene, potresti fare cose con i delegati (BeginInvoke, con un callback che imposta un flag - e il codice originale in attesa di quel flag o timeout) - ma il problema è che è molto difficile chiudere il codice in esecuzione. Ad esempio, uccidere (o mettere in pausa) un thread è pericoloso ... quindi non penso che ci sia un modo semplice per farlo in modo robusto.

Pubblicherò questo, ma nota che non è l'ideale: non ferma l'attività di lunga durata e non si ripulisce correttamente in caso di fallimento.

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

2
Sono perfettamente felice di aver ucciso qualcosa che mi è successo. È ancora meglio che lasciarlo mangiare i cicli della CPU fino al prossimo riavvio (fa parte di un servizio Windows).
Chilltemp,

@Marc: sono un tuo grande fan. Ma, questa volta, mi chiedo, perché non hai usato il risultato. AsyncWaitHandle come menzionato da TheSoftwareJedi. Qualche vantaggio nell'uso di ManualResetEvent su AsyncWaitHandle?
Anand Patel,

1
@Anandoci bene, questo è stato alcuni anni fa, quindi non posso rispondere dalla memoria - ma "facile da capire" conta molto nel codice thread
Marc Gravell

13

Alcuni piccoli cambiamenti alla grande risposta di Pop Catalin:

  • Func invece di Azione
  • Genera eccezione su un valore di timeout errato
  • Chiamata a EndInvoke in caso di timeout

Sono stati aggiunti sovraccarichi per supportare il lavoratore che segnala di annullare l'esecuzione:

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
}

Richiama (e => {// ... if (errore) e.Cancel = true; return 5;}, TimeSpan.FromSeconds (5));
George Tsiokos,

1
Vale la pena sottolineare che in questa risposta il metodo "timeout" viene lasciato in esecuzione a meno che non possa essere modificato per scegliere educatamente di uscire quando contrassegnato con "annulla".
David Eison,

David, questo è il tipo di cancellazione appositamente studiato (.NET 4.0) creato appositamente per indirizzare. In questa risposta, ho usato CancelEventArgs in modo che il lavoratore potesse eseguire il polling di args.Cancel per vedere se dovesse uscire, anche se questo dovrebbe essere implementato di nuovo con il CancelToken per .NET 4.0.
George Tsiokos,

Una nota di utilizzo al riguardo che mi ha confuso per un po ': sono necessari due blocchi try / catch se il codice Function / Action può generare un'eccezione dopo il timeout. È necessario provare / intercettare la chiamata a Invoke per intercettare TimeoutException. È necessario un secondo all'interno della Funzione / Azione per acquisire e ingoiare / registrare qualsiasi eccezione che può verificarsi dopo i tiri di timeout. Altrimenti l'app si chiuderà con un'eccezione non gestita (il mio caso d'uso è il ping test di una connessione WCF su un timeout più stretto di quanto specificato in app.config)
fiat

Assolutamente - poiché il codice all'interno della funzione / azione può essere lanciato, deve trovarsi all'interno di un tentativo / cattura. Per convenzione, questi metodi non tentano di provare / catturare la funzione / azione. È un cattivo design catturare e gettare via l'eccezione. Come per tutto il codice asincrono, spetta all'utente del metodo provare / catturare.
George Tsiokos,

10

Ecco come lo farei:

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

3
questo non interrompe l'attività di esecuzione
TheSoftwareJedi

2
Non tutte le attività sono sicure da interrompere, possono arrivare tutti i tipi di problemi, deadlock, perdite di risorse, corruzione dello stato ... Non dovrebbe essere fatto nel caso generale.
Pop Catalin,

7

L'ho appena eliminato, quindi potrebbe essere necessario qualche miglioramento, ma farà quello che vuoi. È una semplice app console, ma dimostra i principi necessari.

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

}

1
Bello. L'unica cosa che aggiungerei è che potrebbe preferire lanciare System.TimeoutException piuttosto che solo System.Exception
Joel Coehoorn,

Oh, sì: e lo avvolgerei anche nella sua classe.
Joel Coehoorn,

2

Che dire dell'utilizzo di 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");
}

1
Ciò avviserebbe il metodo chiamante di un problema, ma non interromperà il thread offensivo.
Chilltemp,

1
Non sono sicuro che sia corretto. Dalla documentazione non è chiaro cosa succede al thread di lavoro quando scade il timeout di Join.
Matthew Lowe,
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.