Затримки викликів функцій


91

Чи є хороший простий метод затримки виклику функції, дозволяючи потоку продовжувати виконуватись?

напр

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
}

Я знаю, що цього можна досягти за допомогою таймера та обробників подій, але мені було цікаво, чи існує стандартний спосіб # # для досягнення цього?

Якщо комусь цікаво, причиною цього є те, що foo () і bar () знаходяться в різних (одиночних) класах, і мені потрібно телефонувати один одному у виняткових обставинах. Проблема полягає в тому, що це робиться при ініціалізації, тому foo повинен викликати bar, якому потрібен екземпляр створюваного класу foo ... отже, відкладений виклик bar (), щоб переконатися, що foo повністю інстанційований. майже присмаки поганого дизайну!

РЕДАГУВАТИ

Я зважатиму на думки щодо поганого дизайну! Я давно думав, що зможу вдосконалити систему, однак ця неприємна ситуація виникає лише тоді, коли створюється виняток, у будь-який інший час два синглони дуже добре співіснують. Я думаю, що я не збираюся спілкуватися з неприємними асинхронними скоромовками, скоріше, я буду рефакторизувати ініціалізацію одного з класів.


Вам потрібно це виправити, але не за допомогою потоків (або будь-якої іншої практики
асинголізму

1
Необхідність використання потоків для синхронізації ініціалізації об’єкта є знаком того, що вам слід піти іншим шляхом. Оркестратор видається кращим варіантом.
thinkbeforeccoding

1
Воскресіння! - Коментуючи дизайн, ви можете зробити вибір із двоступеневою ініціалізацією. Виходячи з Unity3D API, є Awakeі Startетапи. На Awakeетапі ви налаштовуєте себе, і до кінця цієї фази всі об'єкти ініціалізуються. Під час Startфази об'єкти можуть починати спілкуватися між собою.
cod3monk3y

1
Потрібно змінити прийняту відповідь
Брайан Вебстер

Відповіді:


177

Завдяки сучасній C # 5/6 :)

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

public void bar()
{
    // do stuff
}

15
Ця відповідь є приголомшливою з двох причин. Простота коду та той факт, що Delay НЕ створює потік і не використовує пул потоків, як інші Task.Run або Task.StartNew ... це внутрішній таймер.
Zyo

Гідне рішення.
x4h1d

5
Також зверніть увагу на еквівалентну версію трохи більш чистого (IMO): Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Таран

5
@Zyo Насправді він використовує інший потік. Спробуйте отримати доступ до елемента інтерфейсу з нього, і це спричинить виняток.
TudorT

@TudorT - Якщо Zyo правильно, що він працює на вже існуючому потоці, який запускає події таймера, то його суть полягає в тому, що він не споживає зайвих ресурсів, створюючи новий потік, і не робить черги до пулу потоків. (Хоча я не знаю, чи створення таймера набагато дешевше, ніж чергування завдання на пул потоків - що ТАКОЖ ТАКОЖ не створює потоку, що є цілою суттю пулу потоків.)
ToolmakerSteve

96

Я сам шукав щось подібне - я придумав наступне, хоча він використовує таймер, він використовує його лише один раз для початкової затримки і не вимагає жодних 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
}

(подяка Фреду Дешену за ідею розпоряджатися таймером у зворотному дзвінку)


3
Я вважаю, що це найкраща відповідь взагалі для затримки виклику функції. Немає ниток, не працює фон, немає сну. Таймери дуже ефективні і пам'ять / процесор.
Zyo

1
@Zyo, дякую за ваш коментар - так, таймери ефективні, і така затримка корисна в багатьох ситуаціях, особливо при взаємодії з чимось поза вашим контролем - що не має жодної підтримки для подій сповіщень.
dodgy_coder

Коли ви утилізуєте таймер?
Дідьє А.

1
Відродження старої нитки тут, але таймер міг би бути таким: public static void CallWithDelay (метод дії, int delay) {Timer timer = null; var cb = new TimerCallback ((state) => {method (); timer.Dispose ();}); timer = новий таймер (cb, null, delay, Timeout.Infinite); } РЕДАГУВАТИ: Ну, схоже, ми не можемо розміщувати код у коментарях ... VisualStudio все одно повинен його правильно відформатувати, коли його копіюєте / вставляєте: P
Фред Дешен

6
@dodgy_coder Неправильно. Використання timerлокальної змінної всередині лямбда-сигналу, яка прив’язана до об’єкта-делегата, cbпризводить до того, що вона піднімається до сховища анонімних даних (деталізація реалізації закриття), що призведе до того, що Timerоб’єкт буде доступним з точки зору GC, доки TimerCallbackдоступний сам делегат . Іншими словами, Timerоб’єкт гарантовано не збиратиметься сміття, доки об’єкт-делегат не буде викликаний пулом потоків.
cdhowie

15

Окрім того, що я погодився з дизайнерськими спостереженнями попередніх коментаторів, жодне з рішень не було для мене досить чистим. .Net 4 забезпечує Dispatcherта Taskкласи, які роблять затримку виконання поточного потоку досить простою:

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

Для контексту, я використовую це, щоб розрізнити ICommandприв’язану до лівої кнопки миші вгору на елемент інтерфейсу. Користувачі подвійно клацають, що спричинило всілякі хаоси. (Я знаю, що я міг би також використовувати Click/ DoubleClickобробники, але я хотів рішення, яке працює з ICommands по всій платі).

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

        _execute ();
    }
}

7

Звучить, контроль над створенням обох цих об'єктів та їх взаємозалежність потрібно контролювати зовні, а не між самими класами.


+1, це схоже на те, що вам потрібен якийсь оркестратор і, можливо, завод
ng5000

5

Це справді дуже поганий дизайн, не кажучи вже про синглтон сам по собі поганий дизайн.

Однак якщо вам дійсно потрібно затримати виконання, ось що ви можете зробити:

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

Однак це буде викликано bar()в окремому потоці. Якщо вам потрібно зателефонувати bar()в початковий потік, можливо, вам доведеться перенести bar()виклик до RunWorkerCompletedобробника або трохи зламати SynchronizationContext.


3

Ну, я мав би погодитися з пунктом "проектування" ... але ви, мабуть, можете використовувати монітор, щоб повідомити одного, коли інший пройде критичний розділ ...

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

        }


    }

}

Використання:

DelayedDelegate.Add(MyMethod,5);

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

1
Я порадив би застосувати трохи логіки, щоб уникнути запуску таймера кожні 250 мілісекунд. Перше: Ви можете збільшити затримку до 500 мілісекунд, оскільки ваш мінімально дозволений інтервал становить 1 секунду. По-друге: Ви можете запускати таймер лише тоді, коли додаються нові представники, і зупиняти його, коли вже немає делегатів. Немає причин продовжувати використовувати цикли процесора, коли нічого робити. Третє: ви можете встановити інтервал таймера на мінімальну затримку для всіх делегатів. Тому він прокидається лише тоді, коли йому потрібно викликати делегата, замість того, щоб прокидатися кожні 250 мілісекунд, щоб перевірити, чи є що робити.
Pic Mickael

MethodInvoker - це об’єкт Windows.Forms. Чи є альтернатива для веб-розробників, будь ласка? тобто: те, що не суперечить System.Web.UI.WebControls.
Fandango68

1

Я хоч ідеальним рішенням було б, щоб таймер обробляв відкладену дію. FxCop не любить, коли у вас інтервал менше однієї секунди. Мені потрібно відкласти свої дії, доки ПІСЛЯ мого DataGrid не завершить сортування за стовпцями. Я вважав, що рішенням буде одноразовий таймер (AutoReset = false), і він чудово працює. І, FxCop не дозволить мені придушити попередження!


1

Це буде працювати або на старих версіях .NET
Cons: буде виконуватися у власному потоці

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

    }

Використання:

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

0

Існує не стандартний спосіб затримки виклику функції, окрім використання таймера та подій.

Це звучить як графічний інтерфейс затримки виклику методу, щоб ви могли бути впевнені, що форма закінчила викладати. Не гарна ідея.


0

Спираючись на відповідь Девіда О'Доног'ю, ось оптимізована версія 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();
            }
        }
    }
}

Клас можна було б дещо покращити, використовуючи унікальний ключ для делегатів. Тому що, якщо ви додасте того самого делегата вдруге до того, як був запущений перший, у вас може виникнути проблема зі словником.


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));
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.