Implémenter le délai d'expiration générique C #


157

Je recherche de bonnes idées pour implémenter une manière générique d'exécuter une seule ligne (ou un délégué anonyme) de code avec un timeout.

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

Je recherche une solution qui puisse être mise en œuvre avec élégance dans de nombreux endroits où mon code interagit avec du code capricieux (que je ne peux pas changer).

De plus, j'aimerais que le code «expiré» incriminé cesse de s'exécuter davantage si possible.


46
Juste un rappel à tous ceux qui regardent les réponses ci-dessous: Beaucoup d'entre eux utilisent Thread.Abort qui peut être très mauvais. Veuillez lire les différents commentaires à ce sujet avant d'implémenter Abort dans votre code. Cela peut être approprié à certaines occasions, mais ce sont rares. Si vous ne comprenez pas exactement ce que fait Abort ou n'en avez pas besoin, veuillez implémenter l'une des solutions ci-dessous qui ne l'utilise pas. Ce sont les solutions qui n'ont pas autant de voix parce qu'elles ne répondaient pas aux besoins de ma question.
chilltemp

Merci pour l'avis. +1 vote.
QueueHammer

7
Pour plus de détails sur les dangers de thread.Abort, lisez cet article d'Eric Lippert: blogs.msdn.com/b/ericlippert/archive/2010/02/22/…
JohnW

Réponses:


95

La partie vraiment délicate ici était de tuer la tâche de longue durée en passant le thread de l'exécuteur de l'action à un endroit où il pouvait être abandonné. J'ai accompli cela avec l'utilisation d'un délégué encapsulé qui passe le thread à tuer dans une variable locale dans la méthode qui a créé le lambda.

Je soumets cet exemple, pour votre plaisir. La méthode qui vous intéresse vraiment est CallWithTimeout. Cela annulera le long thread en l'abandonnant et en avalant l'exception ThreadAbortException :

Usage:

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

La méthode statique qui fait le travail:

    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
Pourquoi le catch (ThreadAbortException)? AFAIK vous ne pouvez pas vraiment attraper une ThreadAbortException (elle sera relancée après que le bloc catch soit laissé).
csgero

12
Thread.Abort () est très dangereux à utiliser, il ne doit pas être utilisé avec du code normal, seul le code dont la sécurité est garantie doit être abandonné, tel que le code qui est Cer.Safe, utilise des régions d'exécution contraintes et des poignées sûres. Cela ne devrait être fait pour aucun code.
Pop Catalin

12
Bien que Thread.Abort () soit mauvais, il est loin d'être aussi mauvais qu'un processus hors de contrôle et utilisant chaque cycle CPU et octet de mémoire dont le PC dispose. Mais vous avez raison de signaler les problèmes potentiels à quiconque pense que ce code est utile.
chilltemp

24
Je ne peux pas croire que ce soit la réponse acceptée, quelqu'un ne doit pas lire les commentaires ici, ou la réponse a été acceptée avant les commentaires et cette personne ne vérifie pas sa page de réponses. Thread.Abort n'est pas une solution, c'est juste un autre problème que vous devez résoudre!
Lasse V. Karlsen

18
C'est vous qui ne lisez pas les commentaires. Comme le dit chilltemp ci-dessus, il appelle du code sur lequel il n'a AUCUN contrôle - et veut qu'il soit interrompu. Il n'a pas d'autre choix que Thread.Abort () s'il veut que cela s'exécute dans son processus. Vous avez raison de dire que Thread.Abort est mauvais - mais comme le dit chilltemp, d'autres choses sont pires!
TheSoftwareJedi

73

Nous utilisons beaucoup de code comme celui-ci dans la production :

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

La mise en œuvre est open-source, fonctionne efficacement même dans des scénarios de calcul parallèle et est disponible dans le cadre des bibliothèques partagées 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);
    }
}

Ce code est encore bogué, vous pouvez essayer avec ce petit programme de 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());
      }
   }

Il y a une condition de course. Il est clairement possible qu'une ThreadAbortException soit déclenchée après l' WaitFor<int>.Run()appel de la méthode . Je n'ai pas trouvé de moyen fiable de résoudre ce problème, mais avec le même test, je ne peux pas reprocher de problème avec la réponse acceptée par TheSoftwareJedi .

entrez la description de l'image ici


3
C'est ce que j'ai implémenté, il peut gérer les paramètres et la valeur de retour, ce que je préfère et dont j'ai besoin. Merci Rinat
Gabriel Mongeon

7
qu'est-ce que [Immuable]?
raklos le

2
Juste un attribut que nous utilisons pour marquer les classes immuables (l'immutabilité est vérifiée par Mono Cecil dans les tests unitaires)
Rinat Abdullin

9
C'est une impasse qui attend (je suis surpris que vous ne l'ayez pas encore observé). Votre appel à watchThread.Abort () se trouve dans un verrou, qui doit également être acquis dans le bloc finally. Cela signifie que pendant que le bloc finally attend le verrou (parce que le watchThread l'a entre Wait () retournant et Thread.Abort ()), l'appel watchThread.Abort () bloquera également indéfiniment en attendant que le dernier se termine (ce qu'il ne le sera jamais). Therad.Abort () peut bloquer si une région protégée de code est en cours d'exécution - provoquant des blocages, voir - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev

1
trickdev, merci beaucoup. Pour une raison quelconque, les blocages semblent être très rares, mais nous avons néanmoins corrigé le code :-)
Joannes Vermorel

15

Eh bien, vous pouvez faire des choses avec des délégués (BeginInvoke, avec un rappel définissant un indicateur - et le code d'origine attend cet indicateur ou ce délai d'expiration) - mais le problème est qu'il est très difficile d'arrêter le code en cours d'exécution. Par exemple, tuer (ou mettre en pause) un thread est dangereux ... donc je ne pense pas qu'il existe un moyen facile de le faire de manière robuste.

Je publierai ceci, mais notez que ce n'est pas idéal - cela n'arrête pas la tâche de longue durée et ne nettoie pas correctement en cas d'échec.

    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
Je suis parfaitement heureux de tuer quelque chose qui est devenu rouge sur moi. C'est toujours mieux que de le laisser manger les cycles du processeur jusqu'au prochain redémarrage (cela fait partie d'un service Windows).
chilltemp

@Marc: Je suis un grand fan de la vôtre. Mais, cette fois, je me demande pourquoi vous n'avez pas utilisé result.AsyncWaitHandle comme mentionné par TheSoftwareJedi. Quel est l'avantage d'utiliser ManualResetEvent par rapport à AsyncWaitHandle?
Anand Patel du

1
@Anand eh bien, c'était il y a quelques années donc je ne peux pas répondre de mémoire - mais "facile à comprendre" compte beaucoup dans le code fileté
Marc Gravell

13

Quelques changements mineurs à la grande réponse de Pop Catalin:

  • Func au lieu d'Action
  • Lancer une exception sur une valeur de délai d'expiration incorrecte
  • Appel de EndInvoke en cas d'expiration du délai

Des surcharges ont été ajoutées pour prendre en charge le travailleur de signalisation pour annuler l'exécution:

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
}

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

1
Il vaut la peine de souligner que dans cette réponse, la méthode «timeout» est laissée en cours d'exécution à moins qu'elle ne puisse être modifiée pour choisir poliment de quitter lorsqu'elle est signalée par «annuler».
David Eison

David, c'est pour cela que le type CancellationToken (.NET 4.0) a été spécifiquement créé. Dans cette réponse, j'ai utilisé CancelEventArgs pour que le travailleur puisse interroger args.Cancel pour voir s'il doit se terminer, bien que cela devrait être ré-implémenté avec le CancellationToken pour .NET 4.0.
George Tsiokos

Une note d'utilisation à ce sujet qui m'a dérouté pendant un petit moment: vous avez besoin de deux blocs try / catch si votre code de fonction / action peut lever une exception après l'expiration du délai. Vous avez besoin d'un essai / capture autour de l'appel à Invoke pour intercepter TimeoutException. Vous avez besoin d'une seconde à l'intérieur de votre fonction / action pour capturer et avaler / enregistrer toute exception qui peut se produire après le déclenchement de votre délai. Sinon, l'application se terminera avec une exception non gérée (mon cas d'utilisation est le ping testant une connexion WCF avec un délai d'expiration plus court que celui spécifié dans app.config)
fiat

Absolument - puisque le code à l'intérieur de la fonction / action peut lancer, il doit être à l'intérieur d'un try / catch. Par convention, ces méthodes n'essaient pas d'essayer / d'intercepter la fonction / l'action. C'est une mauvaise conception d'attraper et de jeter l'exception. Comme pour tout code asynchrone, c'est à l'utilisateur de la méthode d'essayer / attraper.
George Tsiokos

10

Voici comment je procéderais:

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
cela n'arrête pas la tâche d'exécution
TheSoftwareJedi

2
Toutes les tâches ne peuvent pas être arrêtées en toute sécurité, toutes sortes de problèmes peuvent arriver, des blocages, des fuites de ressources, une corruption d'état ... Cela ne devrait pas être fait dans le cas général.
Pop Catalin

7

Je viens de le supprimer maintenant, il faudra peut-être une amélioration, mais je ferai ce que vous voulez. C'est une application console simple, mais qui démontre les principes nécessaires.

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
Agréable. La seule chose que j'ajouterais, c'est qu'il préférera peut-être lancer System.TimeoutException plutôt que System.Exception
Joel Coehoorn

Oh, ouais: et j'emballerais cela dans sa propre classe aussi.
Joel Coehoorn

2

Qu'en est-il de l'utilisation de 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
Cela informerait la méthode appelante d'un problème, mais n'annulerait pas le thread incriminé.
chilltemp

1
Je ne suis pas sûr que ce soit exact. La documentation n'indique pas clairement ce qui arrive au thread de travail lorsque le délai de jointure s'écoule.
Matthew Lowe
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.