Appels de fonction retardés


90

Existe-t-il une méthode simple et agréable pour retarder un appel de fonction tout en laissant le thread continuer à s'exécuter?

par exemple

public void foo()
{
    // Do stuff!

    // Delayed call to bar() after x number of ms

    // Do more Stuff
}

public void bar()
{
    // Only execute once foo has finished
}

Je suis conscient que cela peut être réalisé en utilisant un minuteur et des gestionnaires d'événements, mais je me demandais s'il existe un moyen c # standard pour y parvenir?

Si quelqu'un est curieux, la raison pour laquelle cela est nécessaire est que foo () et bar () sont dans des classes (singleton) différentes que j'ai besoin de s'appeler dans des circonstances exceptionnelles. Le problème étant que cela se fait à l'initialisation, donc foo doit appeler bar qui a besoin d'une instance de la classe foo qui est en cours de création ... d'où l'appel retardé à bar () pour s'assurer que foo est complètement instancié. sent presque le mauvais design!

ÉDITER

Je prendrai les points sur la mauvaise conception en délibéré! J'ai longtemps pensé que je pourrais être en mesure d'améliorer le système, cependant, cette situation désagréable ne se produit que lorsqu'une exception est levée, à tous les autres moments, les deux singletons coexistent très bien. Je pense que je ne vais pas me mêler de vilains patters asynchrones, je vais plutôt refactoriser l'initialisation d'une des classes.


Vous devez le réparer, mais pas en utilisant des threads (ou toute autre pratique asynchrone d'ailleurs)
ShuggyCoUk

1
Le fait d'avoir à utiliser des threads pour synchroniser l'initialisation des objets est le signe que vous devez prendre une autre voie. Orchestrator semble une meilleure option.
thinkbeforecoding le

1
Résurrection! - En commentant la conception, vous pouvez faire le choix d'avoir une initialisation en deux étapes. Dessin de l'API Unity3D, il y a Awakeet Startphases. Dans la Awakephase, vous vous configurez et à la fin de cette phase, tous les objets sont initialisés. Pendant la Startphase, les objets peuvent commencer à communiquer entre eux.
cod3monk3y

1
La réponse acceptée doit être modifiée
Brian Webster

Réponses:


171

Merci au C # 5/6 moderne :)

public void foo()
{
    Task.Delay(1000).ContinueWith(t=> bar());
}

public void bar()
{
    // do stuff
}

14
Cette réponse est géniale pour 2 raisons. La simplicité du code et le fait que Delay ne crée PAS de thread ni n'utilise le pool de threads comme les autres Task.Run ou Task.StartNew ... c'est en interne une minuterie.
Zyo le

Une solution décente.
x4h1d

5
Notez également une version équivalente légèrement plus propre (IMO): Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Taran

4
@Zyo En fait, il utilise un fil différent. Essayez d'accéder à un élément de l'interface utilisateur à partir de celui-ci et cela déclenchera une exception.
TudorT

@TudorT - Si Zyo a raison de dire qu'il s'exécute sur un thread déjà existant qui exécute des événements de minuterie, alors son point est qu'il ne consomme pas de ressources supplémentaires en créant un nouveau thread, ni en file d' attente dans le pool de threads. (Bien que je ne sache pas si la création d'une minuterie est beaucoup moins chère que la mise en file d'attente d'une tâche dans le pool de threads - qui AUSSI ne crée pas de thread, c'est tout l'intérêt du pool de threads.)
ToolmakerSteve

96

J'ai cherché quelque chose comme ça moi-même - j'ai trouvé ce qui suit, bien qu'il utilise une minuterie, il ne l'utilise qu'une seule fois pour le délai initial et ne nécessite aucun Sleepappel ...

public void foo()
{
    System.Threading.Timer timer = null; 
    timer = new System.Threading.Timer((obj) =>
                    {
                        bar();
                        timer.Dispose();
                    }, 
                null, 1000, System.Threading.Timeout.Infinite);
}

public void bar()
{
    // do stuff
}

(merci à Fred Deschenes pour l'idée de disposer la minuterie dans le rappel)


3
Je pense que c'est la meilleure réponse en général pour retarder un appel de fonction. Pas de fil, pas de travail en arrière-plan, pas de sommeil. Les minuteries sont très efficaces et en termes de mémoire / processeur.
Zyo le

1
@Zyo, merci pour votre commentaire - oui, les minuteries sont efficaces, et ce type de délai est utile dans de nombreuses situations, en particulier lors de l'interfaçage avec quelque chose qui est hors de votre contrôle - qui ne prend pas en charge les événements de notification.
dodgy_coder

Quand jetez-vous la minuterie?
Didier A.

1
Relancer un ancien thread ici, mais le minuteur pourrait être éliminé comme ceci: public static void CallWithDelay (Méthode d'action, int delay) {Timer timer = null; var cb = new TimerCallback ((state) => {method (); timer.Dispose ();}); timer = new Timer (cb, null, delay, Timeout.Infinite); } EDIT: Eh bien, on dirait que nous ne pouvons pas publier de code dans les commentaires ... VisualStudio devrait le formater correctement lorsque vous le copiez / collez quand même: P
Fred Deschenes

6
@dodgy_coder Faux. L'utilisation de la timervariable locale à partir du lambda qui est liée à l'objet délégué cbentraîne son hissage sur un magasin anon (détail de l'implémentation de la fermeture) qui rendra l' Timerobjet accessible du point de vue du GC tant que le TimerCallbackdélégué lui-même sera accessible . En d'autres termes, il Timerest garanti que l' objet ne sera pas récupéré jusqu'à ce que l'objet délégué soit appelé par le pool de threads.
cdhowie

15

En plus d'être d'accord avec les observations de conception des commentateurs précédents, aucune des solutions n'était assez propre pour moi. .Net 4 fournit Dispatcheret des Taskclasses qui rendent Retardement sur le thread courant assez simple:

static class AsyncUtils
{
    static public void DelayCall(int msec, Action fn)
    {
        // Grab the dispatcher from the current executing thread
        Dispatcher d = Dispatcher.CurrentDispatcher;

        // Tasks execute in a thread pool thread
        new Task (() => {
            System.Threading.Thread.Sleep (msec);   // delay

            // use the dispatcher to asynchronously invoke the action 
            // back on the original thread
            d.BeginInvoke (fn);                     
        }).Start ();
    }
}

Pour le contexte, j'utilise ceci pour éliminer un ICommandlien lié à un bouton gauche de la souris sur un élément de l'interface utilisateur. Les utilisateurs double-cliquent, ce qui cause toutes sortes de ravages. (Je sais que je pourrais aussi utiliser Click/ DoubleClickhandlers, mais je voulais une solution qui fonctionne avec les ICommands dans tous les domaines).

public void Execute(object parameter)
{
    if (!IsDebouncing) {
        IsDebouncing = true;
        AsyncUtils.DelayCall (DebouncePeriodMsec, () => {
            IsDebouncing = false;
        });

        _execute ();
    }
}

7

Il semble que le contrôle de la création de ces deux objets et leur interdépendance doivent être contrôlés de l'extérieur, plutôt qu'entre les classes elles-mêmes.


+1, cela semble que vous ayez besoin d'un orchestrateur quelconque et peut-être d'une usine
ng5000

5

C'est en effet une très mauvaise conception, et encore moins singleton en soi est une mauvaise conception.

Cependant, si vous avez vraiment besoin de retarder l'exécution, voici ce que vous pouvez faire:

BackgroundWorker barInvoker = new BackgroundWorker();
barInvoker.DoWork += delegate
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
        bar();
    };
barInvoker.RunWorkerAsync();

Cela sera cependant appelé bar()sur un thread séparé. Si vous avez besoin d'appeler bar()dans le thread d'origine, vous devrez peut-être déplacer l' bar()invocation vers le RunWorkerCompletedgestionnaire ou faire un peu de piratage avec SynchronizationContext.


3

Eh bien, je devrais être d'accord avec le point de "conception" ... mais vous pouvez probablement utiliser un moniteur pour faire savoir à l'un quand l'autre a dépassé la section critique ...

    public void foo() {
        // Do stuff!

        object syncLock = new object();
        lock (syncLock) {
            // Delayed call to bar() after x number of ms
            ThreadPool.QueueUserWorkItem(delegate {
                lock(syncLock) {
                    bar();
                }
            });

            // Do more Stuff
        } 
        // lock now released, bar can begin            
    }

2
public static class DelayedDelegate
{

    static Timer runDelegates;
    static Dictionary<MethodInvoker, DateTime> delayedDelegates = new Dictionary<MethodInvoker, DateTime>();

    static DelayedDelegate()
    {

        runDelegates = new Timer();
        runDelegates.Interval = 250;
        runDelegates.Tick += RunDelegates;
        runDelegates.Enabled = true;

    }

    public static void Add(MethodInvoker method, int delay)
    {

        delayedDelegates.Add(method, DateTime.Now + TimeSpan.FromSeconds(delay));

    }

    static void RunDelegates(object sender, EventArgs e)
    {

        List<MethodInvoker> removeDelegates = new List<MethodInvoker>();

        foreach (MethodInvoker method in delayedDelegates.Keys)
        {

            if (DateTime.Now >= delayedDelegates[method])
            {
                method();
                removeDelegates.Add(method);
            }

        }

        foreach (MethodInvoker method in removeDelegates)
        {

            delayedDelegates.Remove(method);

        }


    }

}

Usage:

DelayedDelegate.Add(MyMethod,5);

void MyMethod()
{
     MessageBox.Show("5 Seconds Later!");
}

1
Je conseillerais de mettre un peu de logique pour éviter que la minuterie ne s'exécute toutes les 250 millisecondes. Premièrement: vous pouvez augmenter le délai à 500 millisecondes car votre intervalle minimum autorisé est de 1 seconde. Deuxièmement: vous pouvez démarrer le minuteur uniquement lorsque de nouveaux délégués sont ajoutés et l'arrêter lorsqu'il n'y a plus de délégués. Aucune raison de continuer à utiliser les cycles CPU quand il n'y a rien à faire. Troisièmement: vous pouvez régler l'intervalle du minuteur sur le délai minimum pour tous les délégués. Donc, il ne se réveille que lorsqu'il a besoin d'appeler un délégué, au lieu de se réveiller toutes les 250 millisecondes pour voir s'il y a quelque chose à faire.
Pic Mickael du

MethodInvoker est un objet Windows.Forms. Y a-t-il une alternative pour les développeurs Web s'il vous plaît? c'est à dire: quelque chose qui n'entre pas en conflit avec System.Web.UI.WebControls.
Fandango68

1

Je pensais que la solution parfaite serait d'avoir une minuterie pour gérer l'action retardée. FxCop n'aime pas quand vous avez un intervalle de moins d'une seconde. Je dois retarder mes actions jusqu'à APRÈS que mon DataGrid ait terminé le tri par colonne. J'ai pensé qu'une minuterie à un coup (AutoReset = false) serait la solution, et cela fonctionne parfaitement. ET, FxCop ne me laissera pas supprimer l'avertissement!


1

Cela fonctionnera soit sur les anciennes versions de .NET
Cons: s'exécutera dans son propre thread

class CancelableDelay
    {
        Thread delayTh;
        Action action;
        int ms;

        public static CancelableDelay StartAfter(int milliseconds, Action action)
        {
            CancelableDelay result = new CancelableDelay() { ms = milliseconds };
            result.action = action;
            result.delayTh = new Thread(result.Delay);
            result.delayTh.Start();
            return result;
        }

        private CancelableDelay() { }

        void Delay()
        {
            try
            {
                Thread.Sleep(ms);
                action.Invoke();
            }
            catch (ThreadAbortException)
            { }
        }

        public void Cancel() => delayTh.Abort();

    }

Usage:

var job = CancelableDelay.StartAfter(1000, () => { WorkAfter1sec(); });  
job.Cancel(); //to cancel the delayed job

0

Il n'y a pas de méthode standard pour retarder un appel à une fonction autre que d'utiliser une minuterie et des événements.

Cela ressemble au modèle anti-GUI consistant à retarder un appel à une méthode afin que vous puissiez être sûr que le formulaire a terminé sa mise en page. Pas une bonne idée.


0

S'appuyant sur la réponse de David O'Donoghue, voici une version optimisée de Delayed Delegate:

using System.Windows.Forms;
using System.Collections.Generic;
using System;

namespace MyTool
{
    public class DelayedDelegate
    {
       static private DelayedDelegate _instance = null;

        private Timer _runDelegates = null;

        private Dictionary<MethodInvoker, DateTime> _delayedDelegates = new Dictionary<MethodInvoker, DateTime>();

        public DelayedDelegate()
        {
        }

        static private DelayedDelegate Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new DelayedDelegate();
                }

                return _instance;
            }
        }

        public static void Add(MethodInvoker pMethod, int pDelay)
        {
            Instance.AddNewDelegate(pMethod, pDelay * 1000);
        }

        public static void AddMilliseconds(MethodInvoker pMethod, int pDelay)
        {
            Instance.AddNewDelegate(pMethod, pDelay);
        }

        private void AddNewDelegate(MethodInvoker pMethod, int pDelay)
        {
            if (_runDelegates == null)
            {
                _runDelegates = new Timer();
                _runDelegates.Tick += RunDelegates;
            }
            else
            {
                _runDelegates.Stop();
            }

            _delayedDelegates.Add(pMethod, DateTime.Now + TimeSpan.FromMilliseconds(pDelay));

            StartTimer();
        }

        private void StartTimer()
        {
            if (_delayedDelegates.Count > 0)
            {
                int delay = FindSoonestDelay();
                if (delay == 0)
                {
                    RunDelegates();
                }
                else
                {
                    _runDelegates.Interval = delay;
                    _runDelegates.Start();
                }
            }
        }

        private int FindSoonestDelay()
        {
            int soonest = int.MaxValue;
            TimeSpan remaining;

            foreach (MethodInvoker invoker in _delayedDelegates.Keys)
            {
                remaining = _delayedDelegates[invoker] - DateTime.Now;
                soonest = Math.Max(0, Math.Min(soonest, (int)remaining.TotalMilliseconds));
            }

            return soonest;
        }

        private void RunDelegates(object pSender = null, EventArgs pE = null)
        {
            try
            {
                _runDelegates.Stop();

                List<MethodInvoker> removeDelegates = new List<MethodInvoker>();

                foreach (MethodInvoker method in _delayedDelegates.Keys)
                {
                    if (DateTime.Now >= _delayedDelegates[method])
                    {
                        method();

                        removeDelegates.Add(method);
                    }
                }

                foreach (MethodInvoker method in removeDelegates)
                {
                    _delayedDelegates.Remove(method);
                }
            }
            catch (Exception ex)
            {
            }
            finally
            {
                StartTimer();
            }
        }
    }
}

La classe pourrait être légèrement améliorée en utilisant une clé unique pour les délégués. Parce que si vous ajoutez le même délégué une deuxième fois avant le déclenchement du premier, vous risquez de rencontrer un problème avec le dictionnaire.


0
private static volatile List<System.Threading.Timer> _timers = new List<System.Threading.Timer>();
        private static object lockobj = new object();
        public static void SetTimeout(Action action, int delayInMilliseconds)
        {
            System.Threading.Timer timer = null;
            var cb = new System.Threading.TimerCallback((state) =>
            {
                lock (lockobj)
                    _timers.Remove(timer);
                timer.Dispose();
                action()
            });
            lock (lockobj)
                _timers.Add(timer = new System.Threading.Timer(cb, null, delayInMilliseconds, System.Threading.Timeout.Infinite));
}
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.