web-dev-qa-db-fra.com

Appels de fonction retardés

Existe-t-il une méthode simple et agréable pour retarder un appel de fonction tout en laissant le thread continuer à 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 sais que cela peut être réalisé en utilisant une minuterie et des gestionnaires d’événements, mais je me demandais s’il existait une méthode standard c # pour y parvenir.

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

MODIFIER

Je vais prendre les points sur le mauvais design en délibéré! J'ai longtemps pensé que je pourrais peut-être améliorer le système, cependant, cette situation désagréable seulement se produit quand une exception est lancée, les deux singletons coexistent très bien. Je pense que je ne vais pas m'embrouiller avec les méchants modèles asynchrones, mais plutôt que je vais refactoriser l'initialisation de l'une des classes.

65
TK.

Merci au C # 5/6 moderne :)

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

public void bar()
{
    // do stuff
}
114
Korayem

Je cherchais quelque chose comme cela moi-même - je suis arrivé à ce qui suit, bien qu'il utilise une minuterie, il ne l'utilise qu'une fois pour le délai initial et ne nécessite aucun appel Sleep ...

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)

91
dodgy_coder

En plus d'être d'accord avec les observations de conception des précédents commentateurs, aucune des solutions n'était assez propre pour moi. .Net 4 fournit Dispatcher et Task classes qui permettent de retarder l'exécution sur le thread actuel plutôt 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 supprimer une ICommand liée à un bouton gauche de la souris sur un élément de l'interface utilisateur. Les utilisateurs cliquent deux fois sur ce qui causait toutes sortes de dégâts. (Je sais que je pourrais également utiliser des gestionnaires Click/DoubleClick, mais je voulais une solution qui fonctionne avec ICommands dans l’ensemble du tableau).

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

        _execute ();
    }
}
15
cod3monk3y

Cela ressemble au contrôle de la création de ces deux objets et à la nécessité de contrôler leur interdépendance de manière externe plutôt qu'entre les classes elles-mêmes.

7
Adam Ralph

C'est en effet un très mauvais design, encore moins singleton est en soi un mauvais design.

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

Ceci invoquera cependant bar() sur un thread séparé. Si vous devez appeler bar() dans le thread d'origine, vous devrez peut-être déplacer l'invocation de bar() vers le gestionnaire RunWorkerCompleted ou faire un peu de piratage avec SynchronizationContext.

5
Anton Gogolev

Eh bien, je suis d’accord avec le point "design" ... mais vous pouvez probablement utiliser un moniteur pour en informer l’un lorsque 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
Marc Gravell
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!");
}
2
David O'Donoghue

Je pensais que la solution idéale consisterait à laisser une minuterie gérer l'action retardée . FxCop n'aime pas quand vous avez un intervalle inférieur à une seconde . Je dois retarder mes actions jusqu'à ce qu'APRÈS que mon DataGrid ait terminé le tri par colonne . J'ai pensé qu'un minuteur one-shot (AutoReset = false) serait la solution et qu'il fonctionne parfaitement . ET, FxCop ne me laissera pas supprimer l'avertissement!

1
Jim Mahaffey

Cela fonctionnera soit sur les anciennes versions de .NET
Inconvénients: 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
1
altair

Il n'y a pas de moyen standard de retarder un appel vers une fonction autre que d'utiliser un temporisateur et des événements.

Cela ressemble à la méthode anti-interface graphique qui consiste à retarder l'appel d'une méthode afin que vous puissiez être sûr que le formulaire est terminé. Pas une bonne idée.

0
ng5000

Sur la base de la réponse de David O'Donoghue, voici une version optimisée du Délégué retardé:

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 seconde fois avant le premier congédiement, vous pourriez avoir un problème avec le dictionnaire.

0
Pic Mickael
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));
}
0
Koray