158 votes

Implémenter un délai d'attente générique en C#

Je suis à la recherche de bonnes idées pour mettre en œuvre un moyen générique de faire exécuter une seule ligne (ou un délégué anonyme) de code avec un délai d'attente.

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

Je cherche une solution qui puisse être mise en œuvre de manière élégante à de nombreux endroits où mon code interagit avec du code capricieux (que je ne peux pas modifier).

De plus, j'aimerais que le code "timed out" incriminé ne puisse plus s'exécuter si possible.

46 votes

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

0 votes

Merci pour l'avis. +1 vote.

7 votes

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/

96voto

TheSoftwareJedi Points 15921

La partie la plus délicate était de tuer la tâche en cours en faisant passer le fil d'exécution de l'action à un endroit où elle pouvait être interrompue. Pour ce faire, j'ai utilisé un délégué enveloppé qui transmet le fil à tuer dans une variable locale de la méthode qui a créé le lambda.

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

Utilisation :

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 votes

Pourquoi le catch(ThreadAbortException) ? À ma connaissance, on ne peut pas vraiment attraper une ThreadAbortException (elle sera rejetée après la sortie du bloc catch).

0 votes

Vous, mon ami, avez tout à fait raison. Nous pouvons simplement l'ignorer dans ce cas et laisser le fil mourir. Le pool sera réapprovisionné, mais c'est quelque chose à prendre en compte si cela doit se produire souvent. Voici PEUT-ÊTRE un problème de performance.

0 votes

Juste en passant, je ne suis pas sûr de savoir comment cela fonctionne dans .NET. En Java, il faudrait déclarer l'objet Thread comme final, ce qui l'empêcherait de muter. D'une certaine manière, les génies du CLR ont fait en sorte que cela fonctionne. Félicitations.

73voto

Rinat Abdullin Points 13520

Nous utilisons beaucoup de code comme celui-ci dans productio n :

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

L'implémentation est libre, fonctionne efficacement même dans des scénarios de calcul parallèle et est disponible en tant que partie intégrante de la base de données de l'UE. 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 levée après que la méthode WaitFor<int>.Run() est appelé. Je n'ai pas trouvé de moyen fiable de résoudre ce problème, cependant, avec le même test, je ne peux pas reprocher de problème avec la fonction TheSoftwareJedi réponse acceptée.

enter image description here

3 votes

C'est ce que j'ai mis en œuvre, il peut gérer les paramètres et la valeur de retour, ce que je préfère et dont j'avais besoin. Merci Rinat

2 votes

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

0 votes

C'est génial ! Fonctionne très bien.

15voto

Marc Gravell Points 482669

Eh bien, vous pourriez faire des choses avec les délégués (BeginInvoke, avec un callback fixant un drapeau - et le code original attendant ce drapeau ou le timeout) - 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 y ait un moyen facile de le faire de manière robuste.

Je vais poster ceci, mais notez que ce n'est pas idéal - cela n'arrête pas la tâche qui s'éternise, et cela 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 votes

Je suis parfaitement heureux de tuer quelque chose qui est devenu rouge pour moi. C'est toujours mieux que de le laisser manger des cycles CPU jusqu'au prochain redémarrage (cela fait partie d'un service Windows).

0 votes

@Marc : Je suis un grand fan de vous. Mais, cette fois, je me demande pourquoi vous n'avez pas utilisé result.AsyncWaitHandle comme mentionné par TheSoftwareJedi. Y a-t-il un avantage à utiliser ManualResetEvent plutôt que AsyncWaitHandle ?

1 votes

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

13voto

George Tsiokos Points 1008

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

  • Func au lieu de Action
  • Lancer une exception en cas de mauvaise valeur de délai d'attente
  • Appel de EndInvoke en cas de timeout

Des surcharges ont été ajoutées pour permettre de signaler au travailleur d'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
}

0 votes

Invoke(e => { // ... if (error) e.Cancel = true ; return 5 ; }, TimeSpan.FromSeconds(5)) ;

1 votes

Il convient de souligner que dans cette réponse, la méthode "timed out" est laissée en cours d'exécution, à moins qu'elle ne puisse être modifiée pour choisir poliment de sortir lorsqu'elle est signalée par "cancel".

0 votes

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 savoir s'il doit se retirer, bien que cela doive être réimplémenté avec le CancellationToken pour .NET 4.0.

10voto

Pop Catalin Points 25033

C'est comme ça que je ferais :

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 votes

Cela n'arrête pas l'exécution de la tâche

2 votes

Il n'est pas sûr d'arrêter toutes les tâches, toutes sortes de problèmes peuvent survenir : blocages, fuites de ressources, corruption d'état... Cela ne devrait pas être fait dans le cas général.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X