J'ai une série de blocs de code qui prennent trop de temps. Je n'ai besoin d'aucune finesse en cas d'échec. En fait, je veux lever une exception lorsque ces blocs prennent trop de temps et tombent simplement grâce à notre gestion d'erreur standard. Je préférerais ne PAS créer de méthodes à partir de chaque bloc (qui sont les seules suggestions que j'ai vues jusqu'à présent), car cela nécessiterait une réécriture majeure de la base de code.

Voici ce que j'aimerais AIMER créer, si possible.

public void MyMethod( ... )
{

 ...

    using (MyTimeoutObject mto = new MyTimeoutObject(new TimeSpan(0,0,30)))
    {
        // Everything in here must complete within the timespan
        // or mto will throw an exception. When the using block
        // disposes of mto, then the timer is disabled and 
        // disaster is averted.
    }

 ...
}

J'ai créé un objet simple pour ce faire en utilisant la classe Timer. (NOTE pour ceux qui aiment copier / coller: CE CODE NE FONCTIONNE PAS !!)

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

    public class MyTimeoutObject : IDisposable
    {
        private Timer timer = null;

        public MyTimeoutObject (TimeSpan ts)
        {
            timer = new Timer();
            timer.Elapsed += timer_Elapsed;
            timer.Interval = ts.TotalMilliseconds;

            timer.Start();
        }

        void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            throw new TimeoutException("A code block has timed out.");
        }

        public void Dispose()
        {
            if (timer != null)
            {
                timer.Stop();
            }
        }
    }

Cela ne fonctionne pas car la classe System.Timers.Timer capture, absorbe et ignore toutes les exceptions lancées, ce qui - comme je l'ai découvert - va à l'encontre de ma conception. Une autre façon de créer cette classe / fonctionnalité sans une refonte totale?

Cela semblait si simple il y a deux heures, mais me cause beaucoup de maux de tête.

7
Jerry 4 janv. 2016 à 21:09

3 réponses

Meilleure réponse

OK, j'ai passé du temps sur celui-ci et je pense avoir une solution qui fonctionnera pour vous sans avoir à changer trop votre code.

Voici comment vous utiliseriez la classe Timebox que j'ai créée.

public void MyMethod( ... ) {

    // some stuff

    // instead of this
    // using(...){ /* your code here */ }

    // you can use this
    var timebox = new Timebox(TimeSpan.FromSeconds(1));
    timebox.Execute(() =>
    {
        /* your code here */
    });

    // some more stuff

}

Voici comment Timebox fonctionne.

  • Un objet Timebox est créé avec un Timespan donné
  • Lorsque Execute est appelé, le Timebox crée un enfant AppDomain pour contenir une référence d'objet TimeboxRuntime, et lui renvoie un proxy
  • L'objet TimeboxRuntime dans l'enfant AppDomain prend un Action comme entrée pour s'exécuter dans le domaine enfant
  • Timebox crée ensuite une tâche pour appeler le proxy TimeboxRuntime
  • La tâche est lancée (et l'exécution de l'action démarre), et le thread "principal" attend aussi longtemps que le TimeSpan donné
  • Après le TimeSpan donné (ou lorsque la tâche est terminée), l'enfant AppDomain est déchargé, que le Action soit terminé ou non.
  • Un TimeoutException est lancé si action expire, sinon si action lance une exception, il est intercepté par l'enfant AppDomain et renvoyé pour l'appel AppDomain à jeter

Un inconvénient est que votre programme aura besoin d'autorisations suffisamment élevées pour créer un AppDomain.

Voici un exemple de programme qui montre comment cela fonctionne (je crois que vous pouvez copier-coller ceci, si vous incluez les using corrects). J'ai également créé cet essentiel si vous êtes intéressé.

public class Program
{
    public static void Main()
    {
        try
        {
            var timebox = new Timebox(TimeSpan.FromSeconds(1));
            timebox.Execute(() =>
            {
                // do your thing
                for (var i = 0; i < 1000; i++)
                {
                    Console.WriteLine(i);
                }
            });

            Console.WriteLine("Didn't Time Out");
        }
        catch (TimeoutException e)
        {
            Console.WriteLine("Timed Out");
            // handle it
        }
        catch(Exception e)
        {
            Console.WriteLine("Another exception was thrown in your timeboxed function");
            // handle it
        }
        Console.WriteLine("Program Finished");
        Console.ReadLine();
    }
}

public class Timebox
{
    private readonly TimeSpan _ts;

    public Timebox(TimeSpan ts)
    {
        _ts = ts;
    }

    public void Execute(Action func)
    {
        AppDomain childDomain = null;
        try
        {
            // Construct and initialize settings for a second AppDomain.  Perhaps some of
            // this is unnecessary but perhaps not.
            var domainSetup = new AppDomainSetup()
            {
                ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
                ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName,
                LoaderOptimization = LoaderOptimization.MultiDomainHost
            };

            // Create the child AppDomain
            childDomain = AppDomain.CreateDomain("Timebox Domain", null, domainSetup);

            // Create an instance of the timebox runtime child AppDomain
            var timeboxRuntime = (ITimeboxRuntime)childDomain.CreateInstanceAndUnwrap(
                typeof(TimeboxRuntime).Assembly.FullName, typeof(TimeboxRuntime).FullName);

            // Start the runtime, by passing it the function we're timboxing
            Exception ex = null;
            var timeoutOccurred = true;
            var task = new Task(() =>
            {
                ex = timeboxRuntime.Run(func);
                timeoutOccurred = false;
            });

            // start task, and wait for the alloted timespan.  If the method doesn't finish
            // by then, then we kill the childDomain and throw a TimeoutException
            task.Start();
            task.Wait(_ts);

            // if the timeout occurred then we throw the exception for the caller to handle.
            if(timeoutOccurred)
            {
                throw new TimeoutException("The child domain timed out");
            }

            // If no timeout occurred, then throw whatever exception was thrown
            // by our child AppDomain, so that calling code "sees" the exception
            // thrown by the code that it passes in.
            if(ex != null)
            {
                throw ex;
            }
        }
        finally
        {
            // kill the child domain whether or not the function has completed
            if(childDomain != null) AppDomain.Unload(childDomain);
        }
    }

    // don't strictly need this, but I prefer having an interface point to the proxy
    private interface ITimeboxRuntime
    {
        Exception Run(Action action);
    }

    // Need to derive from MarshalByRefObject... proxy is returned across AppDomain boundary.
    private class TimeboxRuntime : MarshalByRefObject, ITimeboxRuntime
    {
        public Exception Run(Action action)
        {
            try
            {
                // Nike: just do it!
                action();
            }
            catch(Exception e)
            {
                // return the exception to be thrown in the calling AppDomain
                return e;
            }
            return null;
        }
    }
}

ÉDITER:

La raison pour laquelle j'ai opté pour un AppDomain au lieu de Thread ou Task uniquement, c'est qu'il n'y a pas de moyen à toute épreuve pour mettre fin à Thread ou Task s pour le code arbitraire [1] [2] [3]. Un AppDomain, pour vos besoins, me semblait être la meilleure approche.

2
Community 23 mai 2017 à 12:16

Voici une implémentation asynchrone des délais d'expiration:

   ...
      private readonly semaphore = new SemaphoreSlim(1,1);

   ...
      // total time allowed here is 100ms
      var tokenSource = new CancellationTokenSource(100); 
      try{
        await WorkMethod(parameters, tokenSource.Token); // work 
      } catch (OperationCancelledException ocx){
        // gracefully handle cancellations:
        label.Text = "Operation timed out";
      }
   ...  

    public async Task WorkMethod(object prm, CancellationToken ct){
      try{
        await sem.WaitAsync(ct); // equivalent to lock(object){...}
        // synchronized work, 
        // call  tokenSource.Token.ThrowIfCancellationRequested() or
        // check tokenSource.IsCancellationRequested in long-running blocks
        // and pass ct to other tasks, such as async HTTP or stream operations
      } finally {
        sem.Release();
      }
    }

PAS que je le conseille, mais vous pourriez passer le tokenSource au lieu de son Token dans WorkMethod et faire périodiquement tokenSource.CancelAfter(200) pour ajouter plus de temps si vous êtes certain que vous n'êtes pas à un endroit qui peut être bloqué (en attente d'un appel HTTP) mais je pense que ce serait une approche ésotérique du multithreading.

Au lieu de cela, vos threads doivent être aussi rapides que possible (minimum d'E / S) et un thread peut sérialiser les ressources (producteur) tandis que d'autres traitent une file d'attente (consommateurs) si vous devez gérer le multithreading IO (par exemple, compression de fichiers, téléchargements, etc.) et éviter les blocages possibilité tout à fait.

2
Sten Petrov 4 janv. 2016 à 19:24

J'ai vraiment aimé l'idée visuelle d'une déclaration d'utilisation. Cependant , ce n’est pas une solution viable. Pourquoi? Eh bien, un sous-thread (l'objet / thread / timer dans l'instruction using) ne peut pas perturber le thread principal et injecter une exception, ce qui l'oblige à arrêter ce qu'il faisait et à sauter au try / catch le plus proche. C'est à cela que tout se résume. Plus je me suis assis et j'ai travaillé avec cela, plus cela s'est révélé.

Bref, ça ne peut pas être fait comme je le voulais.

Cependant, j'ai adopté l'approche de Pieter et j'ai un peu mutilé mon code. Cela introduit des problèmes de lisibilité, mais j'ai essayé de les atténuer avec des commentaires et autres.

public void MyMethod( ... )
{

 ...

    // Placeholder for thread to kill if the action times out.
    Thread threadToKill = null;
    Action wrappedAction = () => 
    {
        // Take note of the action's thread. We may need to kill it later.
        threadToKill = Thread.CurrentThread;

        ...
        /* DO STUFF HERE */
        ...

    };

    // Now, execute the action. We'll deal with the action timeouts below.
    IAsyncResult result = wrappedAction.BeginInvoke(null, null);

    // Set the timeout to 10 minutes.
    if (result.AsyncWaitHandle.WaitOne(10 * 60 * 1000))
    {
        // Everything was successful. Just clean up the invoke and get out.
        wrappedAction.EndInvoke(result);
    }
    else 
    {
        // We have timed out. We need to abort the thread!! 
        // Don't let it continue to try to do work. Something may be stuck.
        threadToKill.Abort();
        throw new TimeoutException("This code block timed out");
    }

 ...
}

Puisque je fais cela à trois ou quatre endroits par section principale, cela devient plus difficile à lire. Cependant, cela fonctionne assez bien.

0
Jerry 4 janv. 2016 à 21:19