Verzögerte Funktionsaufrufe

90

Gibt es eine nette einfache Methode, um einen Funktionsaufruf zu verzögern, während der Thread weiter ausgeführt wird?

z.B

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
}

Ich bin mir bewusst, dass dies mit einem Timer und Ereignishandlern erreicht werden kann, aber ich habe mich gefragt, ob es einen Standardweg gibt, um dies zu erreichen.

Wenn jemand neugierig ist, ist der Grund dafür, dass foo () und bar () in verschiedenen (Singleton-) Klassen sind, die ich unter außergewöhnlichen Umständen gegenseitig aufrufen muss. Das Problem ist, dass dies bei der Initialisierung erfolgt, sodass foo bar aufrufen muss, das eine Instanz der erstellten foo-Klasse benötigt ... daher der verzögerte Aufruf von bar (), um sicherzustellen, dass foo vollständig instanziiert ist. Lesen Sie dies zurück fast riecht nach schlechtem Design!

BEARBEITEN

Ich werde die Punkte über schlechtes Design unter Hinweis nehmen! Ich habe lange gedacht, dass ich das System verbessern könnte, aber diese schlimme Situation tritt nur auf, wenn eine Ausnahme ausgelöst wird, zu allen anderen Zeiten existieren die beiden Singletons sehr gut nebeneinander. Ich denke, ich werde mich nicht mit fiesen Async-Mustern herumschlagen, sondern die Initialisierung einer der Klassen umgestalten.

TK.
quelle
Sie müssen das Problem beheben, aber nicht mithilfe von Threads (oder einer anderen asynchrone Vorgehensweise)
ShuggyCoUk,
1
Die Verwendung von Threads zum Synchronisieren der Objektinitialisierung ist das Zeichen dafür, dass Sie einen anderen Weg einschlagen sollten. Orchestrator scheint eine bessere Option zu sein.
thinkbeforecoding
1
Auferstehung! - Wenn Sie das Design kommentieren, können Sie sich für eine zweistufige Initialisierung entscheiden. Zeichnung aus dem Unity3D API gibt es Awakeund StartPhasen. In der AwakePhase konfigurieren Sie sich selbst und am Ende dieser Phase werden alle Objekte initialisiert. Während der StartPhase können die Objekte miteinander kommunizieren.
cod3monk3y
1
Die akzeptierte Antwort muss geändert werden
Brian Webster

Antworten:

169

Dank modernem C # 5/6 :)

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

public void bar()
{
    // do stuff
}
Korayem
quelle
14
Diese Antwort ist aus zwei Gründen fantastisch. Code-Einfachheit und die Tatsache, dass Delay KEINEN Thread erstellt oder den Thread-Pool wie andere Task.Run oder Task.StartNew verwendet ... es ist intern ein Timer.
Zyo
Eine anständige Lösung.
x4h1d
5
Beachten Sie auch eine etwas sauberere (IMO) äquivalente Version: Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Taran
4
@Zyo Eigentlich verwendet es einen anderen Thread. Wenn Sie versuchen, von dort aus auf ein UI-Element zuzugreifen, wird eine Ausnahme ausgelöst.
TudorT
@TudorT - Wenn Zyo richtig ist , dass es läuft auf einem bereits bestehenden Thread, läuft Timer - Ereignisse, dann seinen Punkt ist , dass es keine zusätzlichen Ressourcen verbrauchen die Schaffung eines neuen Threads, noch Schlange stehen an den Thread - Pool. (Obwohl ich nicht weiß, ob das Erstellen eines Timers wesentlich billiger ist als das Einreihen einer Aufgabe in den Thread-Pool - der AUCH keinen Thread erstellt, das ist der springende Punkt des Thread-Pools.)
ToolmakerSteve
96

Ich habe selbst nach so etwas gesucht - ich habe mir Folgendes ausgedacht, obwohl es einen Timer verwendet, ihn nur einmal für die anfängliche Verzögerung verwendet und keine SleepAnrufe erfordert ...

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
}

(Danke an Fred Deschenes für die Idee, den Timer innerhalb des Rückrufs zu entsorgen)

dodgy_coder
quelle
3
Ich halte dies im Allgemeinen für die beste Antwort, um einen Funktionsaufruf zu verzögern. Kein Thread, kein Hintergrund, kein Schlaf. Timer sind sehr effizient und speicher- / CPU-weise.
Zyo
1
@Zyo, danke für Ihren Kommentar - ja, Timer sind effizient, und diese Art von Verzögerung ist in vielen Situationen nützlich, insbesondere wenn Sie eine Schnittstelle zu etwas herstellen, das außerhalb Ihrer Kontrolle liegt - und das keine Unterstützung für Benachrichtigungsereignisse bietet.
dodgy_coder
Wann entsorgen Sie den Timer?
Didier A.
1
Hier wird ein alter Thread wiederbelebt, aber der Timer könnte folgendermaßen angeordnet werden: public static void CallWithDelay (Aktionsmethode, int delay) {Timer timer = null; var cb = new TimerCallback ((state) => {method (); timer.Dispose ();}); timer = neuer Timer (cb, null, delay, Timeout.Infinite); } EDIT: Nun, es sieht so aus, als könnten wir keinen Code in Kommentaren posten ... VisualStudio sollte ihn trotzdem richtig formatieren, wenn Sie ihn kopieren / einfügen: P
Fred Deschenes
6
@dodgy_coder Falsch. Wenn Sie die timerlokale Variable aus dem Lambda heraus verwenden, die an das Delegatenobjekt gebunden wird, cbwird sie in einen anon-Speicher (Detail der Abschlussimplementierung) gehievt, wodurch das TimerObjekt aus Sicht des GC erreichbar ist, solange der TimerCallbackDelegat selbst erreichbar ist . Mit anderen Worten, es Timerwird garantiert , dass das Objekt erst dann mit Müll gesammelt wird, wenn das Delegatenobjekt vom Thread-Pool aufgerufen wird.
CDhowie
16

Abgesehen davon, dass ich den Entwurfsbeobachtungen der vorherigen Kommentatoren zustimmte, war keine der Lösungen für mich sauber genug. .Net 4 bietet Dispatcherund TaskKlassen, die die Verzögerung der Ausführung im aktuellen Thread ziemlich einfach machen:

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

Für den Kontext verwende ich dies, um eine ICommandan eine linke Maustaste gebundene Schaltfläche auf einem UI-Element zu entprellen . Benutzer doppelklicken, was alle Arten von Chaos verursachte. (Ich weiß, dass ich auch Click/ DoubleClickhandler verwenden könnte, aber ich wollte eine Lösung, die auf ganzer Linie mit ICommands funktioniert ).

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

        _execute ();
    }
}
cod3monk3y
quelle
7

Es klingt so, als ob die Kontrolle über die Erstellung dieser beiden Objekte und ihre gegenseitige Abhängigkeit extern und nicht zwischen den Klassen selbst gesteuert werden muss.

Adam Ralph
quelle
+1, das hört sich so an, als ob Sie einen Orchestrator und vielleicht eine Fabrik
brauchen
5

Es ist in der Tat ein sehr schlechtes Design, geschweige denn Singleton an sich ist schlechtes Design.

Wenn Sie die Ausführung jedoch wirklich verzögern müssen, können Sie Folgendes tun:

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

Dies wird jedoch bar()in einem separaten Thread aufgerufen. Wenn Sie bar()den ursprünglichen Thread aufrufen müssen, müssen Sie möglicherweise den bar()Aufruf an den RunWorkerCompletedHandler verschieben oder ein bisschen hacken SynchronizationContext.

Anton Gogolev
quelle
3

Nun, ich müsste dem "Design" -Punkt zustimmen ... aber Sie können wahrscheinlich einen Monitor verwenden, um einen zu informieren, wenn der andere den kritischen Abschnitt überschritten hat ...

    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            
    }
Marc Gravell
quelle
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);

        }


    }

}

Verwendung:

DelayedDelegate.Add(MyMethod,5);

void MyMethod()
{
     MessageBox.Show("5 Seconds Later!");
}
David O'Donoghue
quelle
1
Ich würde empfehlen, eine Logik zu verwenden, um zu vermeiden, dass der Timer alle 250 Millisekunden ausgeführt wird. Erstens: Sie können die Verzögerung auf 500 Millisekunden erhöhen, da Ihr minimal zulässiges Intervall 1 Sekunde beträgt. Zweitens: Sie können den Timer nur starten, wenn neue Delegaten hinzugefügt werden, und ihn stoppen, wenn keine Delegaten mehr vorhanden sind. Kein Grund, weiterhin CPU-Zyklen zu verwenden, wenn nichts zu tun ist. Drittens: Sie können das Timer-Intervall für alle Delegierten auf die minimale Verzögerung einstellen. Es wird also nur aktiviert, wenn ein Delegat aufgerufen werden muss, anstatt alle 250 Millisekunden aufzuwachen, um festzustellen, ob etwas zu tun ist.
Pic Mickael
MethodInvoker ist ein Windows.Forms-Objekt. Gibt es bitte eine Alternative für Webentwickler? dh: etwas, das nicht mit System.Web.UI.WebControls kollidiert.
Fandango68
1

Ich denke, die perfekte Lösung wäre, einen Timer für die verzögerte Aktion zu haben. FxCop mag es nicht, wenn Sie ein Intervall von weniger als einer Sekunde haben. Ich muss meine Aktionen verzögern, bis mein DataGrid die Sortierung nach Spalten abgeschlossen hat. Ich dachte, ein One-Shot-Timer (AutoReset = false) wäre die Lösung, und er funktioniert perfekt. UND, FxCop lässt mich die Warnung nicht unterdrücken!

Jim Mahaffey
quelle
1

Dies funktioniert entweder mit älteren Versionen von .NET
Cons: wird in einem eigenen Thread ausgeführt

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

    }

Verwendung:

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

Es gibt keine Standardmethode, um einen Aufruf einer Funktion zu verzögern, außer einen Timer und Ereignisse zu verwenden.

Dies klingt wie das GUI-Anti-Muster zum Verzögern eines Aufrufs einer Methode, damit Sie sicher sein können, dass das Formular vollständig angelegt wurde. Keine gute Idee.

ng5000
quelle
0

Aufbauend auf der Antwort von David O'Donoghue finden Sie hier eine optimierte Version des verzögerten Delegierten:

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

Die Klasse könnte durch Verwendung eines eindeutigen Schlüssels für die Delegierten etwas verbessert werden. Wenn Sie denselben Delegaten ein zweites Mal hinzufügen, bevor der erste ausgelöst wurde, tritt möglicherweise ein Problem mit dem Wörterbuch auf.

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