Opóźnione wywołania funkcji


92

Czy istnieje ładna, prosta metoda opóźnienia wywołania funkcji, pozwalając jednocześnie wątkowi kontynuować wykonywanie?

na przykład

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
}

Zdaję sobie sprawę, że można to osiągnąć za pomocą licznika czasu i programów obsługi zdarzeń, ale zastanawiałem się, czy istnieje standardowy sposób C #, aby to osiągnąć?

Jeśli ktoś jest ciekawy, powodem, dla którego jest to wymagane, jest to, że foo () i bar () są w różnych (singleton) klasach, które muszę nazywać się w wyjątkowych okolicznościach. Problem polega na tym, że odbywa się to podczas inicjalizacji, więc foo musi wywołać bar, który potrzebuje instancji klasy foo, która jest tworzona ... stąd opóźnione wywołanie bar (), aby upewnić się, że foo jest w pełni zainicjowane. Czytanie tego z powrotem prawie trąci złym projektem!

EDYTOWAĆ

Rozważę punkty dotyczące złego projektu! Od dawna myślałem, że może uda mi się ulepszyć system, jednak ta paskudna sytuacja występuje tylko wtedy, gdy zostanie zgłoszony wyjątek, we wszystkich innych przypadkach dwa singletony bardzo ładnie współistnieją. Myślę, że nie zamierzam mieszać z paskudnymi wzorami asynchronicznymi, raczej zamierzam refaktoryzować inicjalizację jednej z klas.


Musisz to naprawić, ale nie za pomocą wątków (lub jakiejkolwiek innej asynowej praktyki w tym zakresie)
ShuggyCoUk

1
Konieczność używania wątków do synchronizacji inicjalizacji obiektów jest znakiem, że należy obrać inną drogę. Orchestrator wydaje się lepszą opcją.
pomyśl przed kodowaniem

1
Wskrzeszenie! - Komentując projekt, można było dokonać wyboru dwustopniowej inicjalizacji. Czerpiąc z interfejsu API Unity3D, istnieją Awakei Startfazy. W tej Awakefazie konfigurujesz siebie, a pod koniec tej fazy wszystkie obiekty są inicjalizowane. W tej Startfazie obiekty mogą zacząć komunikować się ze sobą.
cod3monk3y

1
Zaakceptowana odpowiedź musi zostać zmieniona
Brian Webster

Odpowiedzi:


178

Dzięki nowoczesnemu C # 5/6 :)

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

public void bar()
{
    // do stuff
}

15
Ta odpowiedź jest niesamowita z 2 powodów. Prostota kodu i fakt, że Delay NIE tworzy wątku ani nie używa puli wątków jak inne Task.Run lub Task.StartNew ... jest to wewnętrznie zegar.
Zyo

Przyzwoite rozwiązanie.
x4h1d

6
Zwróć również uwagę na nieco czystszą wersję (IMO): Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Taran

5
@Zyo Właściwie używa innego wątku. Spróbuj uzyskać z niego dostęp do elementu interfejsu użytkownika, a spowoduje to wyjątek.
TudorT

@TudorT - Jeśli Zyo ma rację, że działa na już istniejącym wątku, który uruchamia zdarzenia czasomierza, to jego chodzi o to, że nie zużywa dodatkowych zasobów, tworząc nowy wątek, ani kolejkując do puli wątków. (Chociaż nie wiem, czy tworzenie timera jest znacznie tańsze niż kolejkowanie zadania do puli wątków - co RÓWNIEŻ nie tworzy wątku, co jest całym celem puli wątków.)
ToolmakerSteve

96

Sam szukałem czegoś takiego - wymyśliłem co następuje, chociaż korzysta z timera, używa go tylko raz do początkowego opóźnienia i nie wymaga żadnych Sleeppołączeń ...

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
}

(podziękowania dla Freda Deschenesa za pomysł pozbycia się timera w wywołaniu zwrotnym)


3
Uważam, że jest to ogólnie najlepsza odpowiedź na opóźnienie wywołania funkcji. Bez wątku, bez pracy w tle, bez snu. Liczniki czasu są bardzo wydajne i mądre pod względem pamięci / procesora.
Zyo,

1
@Zyo, dziękuję za komentarz - tak, liczniki czasu są wydajne, a tego rodzaju opóźnienie jest przydatne w wielu sytuacjach, szczególnie podczas łączenia się z czymś, na co nie masz kontroli - co nie obsługuje zdarzeń powiadomień.
dodgy_coder

Kiedy pozbywasz się timera?
Didier A.

1
Przywracanie starego wątku tutaj, ale licznik czasu można usunąć w ten sposób: public static void CallWithDelay (metoda akcji, opóźnienie int) {Timer timer = null; var cb = new TimerCallback ((state) => {method (); timer.Dispose ();}); timer = new Timer (cb, null, delay, Timeout.Infinite); } EDYCJA: Cóż, wygląda na to, że nie możemy umieszczać kodu w komentarzach ... VisualStudio powinno go poprawnie sformatować po skopiowaniu / wklejeniu: P
Fred Deschenes

6
@dodgy_coder Wrong. Użycie timerzmiennej lokalnej z poziomu lambda, która jest powiązana z obiektem delegata, cbpowoduje, że jest ona przenoszona do magazynu anon (szczegóły implementacji zamknięcia), co spowoduje, że Timerobiekt będzie dostępny z perspektywy GC tak długo, jak TimerCallbacksam delegat jest osiągalny . Innymi słowy, gwarantuje się , że Timerobiekt nie zostanie wyrzucony do pamięci, dopóki obiekt delegata nie zostanie wywołany przez pulę wątków.
cdhowie

15

Pomijając zgodzenie się z obserwacjami projektowymi poprzednich komentatorów, żadne z rozwiązań nie było dla mnie wystarczająco czyste. .Net 4 zapewnia Dispatcheri Taskklasy, które sprawiają, że opóźnianie wykonania bieżącego wątku jest dość proste:

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

W kontekście używam tego do odbicia ICommandpowiązanego z lewym przyciskiem myszy w elemencie interfejsu użytkownika. Użytkownicy klikali dwukrotnie, co powodowało różnego rodzaju spustoszenie. (Wiem, że mógłbym również użyć Click/ DoubleClickhandlers, ale chciałem rozwiązania, które działa z ICommandwszystkimi elementami).

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

        _execute ();
    }
}

7

Wygląda na to, że kontrola tworzenia obu tych obiektów i ich współzależności musi być kontrolowana zewnętrznie, a nie między samymi klasami.


+1, brzmi to tak, jakbyś potrzebował jakiegoś orkiestratora i być może fabryki
ng5000

5

To rzeczywiście bardzo zły projekt, nie mówiąc już o singletonie sam w sobie jest złym projektem.

Jeśli jednak naprawdę musisz opóźnić wykonanie, oto co możesz zrobić:

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

Spowoduje to jednak wywołanie bar()w osobnym wątku. Jeśli chcesz zadzwonić bar()w oryginalnym wątku, może być konieczne przeniesienie bar()wywołania do modułu RunWorkerCompletedobsługi lub trochę włamania SynchronizationContext.


3

Cóż, musiałbym zgodzić się z punktem „projektowania” ... ale prawdopodobnie możesz użyć monitora, aby powiadomić jednego, gdy drugi znajdzie się poza krytyczną sekcją ...

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

        }


    }

}

Stosowanie:

DelayedDelegate.Add(MyMethod,5);

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

1
Radziłbym zastosować logikę, aby uniknąć uruchamiania timera co 250 milisekund. Po pierwsze: możesz zwiększyć opóźnienie do 500 milisekund, ponieważ minimalny dozwolony interwał wynosi 1 sekundę. Po drugie: odliczanie czasu można uruchomić tylko wtedy, gdy zostaną dodani nowi pełnomocnicy, i zatrzymać, gdy nie ma już delegatów. Nie ma powodu, aby nadal używać cykli procesora, gdy nie ma nic do zrobienia. Po trzecie: możesz ustawić interwał timera na minimalne opóźnienie dla wszystkich delegatów. Dlatego budzi się tylko wtedy, gdy musi przywołać delegata, zamiast budzić się co 250 milisekund, aby sprawdzić, czy jest coś do zrobienia.
Pic Mickael,

MethodInvoker jest obiektem Windows.Forms. Czy istnieje alternatywa dla twórców stron internetowych? tj .: coś, co nie koliduje z System.Web.UI.WebControls.
Fandango68

1

Pomyślałem, że idealnym rozwiązaniem byłoby posiadanie timera do obsługi opóźnionego działania. FxCop nie lubi, gdy interwał jest krótszy niż jedna sekunda. Muszę opóźnić moje działania do PO zakończeniu sortowania według kolumny DataGrid. Pomyślałem, że rozwiązaniem będzie jednorazowy licznik czasu (AutoReset = false) i działa idealnie. I FxCop nie pozwoli mi zlikwidować ostrzeżenia!


1

Będzie to działać na starszych wersjach .NET
Wady: będzie działać w swoim własnym wątku

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

    }

Stosowanie:

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

0

Nie ma standardowego sposobu opóźnienia wywołania funkcji innej niż użycie timera i zdarzeń.

To brzmi jak wzorzec przeciwwagi GUI opóźniający wywołanie metody, dzięki czemu możesz mieć pewność, że formularz został ukończony. Nie jest to dobry pomysł.


0

Opierając się na odpowiedzi Davida O'Donoghue'a, oto zoptymalizowana wersja Opóźnionego delegata:

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

Klasę można nieco ulepszyć, używając unikalnego klucza dla delegatów. Ponieważ jeśli dodasz tego samego delegata po raz drugi przed uruchomieniem pierwszego, możesz mieć problem ze słownikiem.


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));
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.