Отложенные вызовы функций


92

Есть ли хороший простой способ отложить вызов функции, позволяя потоку продолжать выполнение?

например

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
}

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

Если кому-то интересно, причина, по которой это требуется, заключается в том, что foo () и bar () находятся в разных (одноэлементных) классах, которые мне нужно вызывать друг друга в исключительных обстоятельствах. Проблема в том, что это делается при инициализации, поэтому foo должен вызвать bar, которому нужен экземпляр класса foo, который создается ... отсюда отложенный вызов bar (), чтобы гарантировать, что foo полностью создан. почти попахивает плохим дизайном!

РЕДАКТИРОВАТЬ

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


Вам нужно исправить это, но не с помощью потоков (или любой другой асинхронной практики в этом отношении)
ShuggyCoUk

1
Необходимость использовать потоки для синхронизации инициализации объекта - признак того, что вам следует пойти другим путем. Orchestrator кажется лучшим вариантом.
thinkbeforecoding

1
Воскрешение! - Комментируя дизайн, можно было сделать выбор в пользу двухэтапной инициализации. Опираясь на Unity3D API, есть Awakeи Startфазы. На Awakeэтапе вы настраиваете себя, и к концу этого этапа все объекты инициализируются. Во время Startфазы объекты могут начать общаться друг с другом.
cod3monk3y 05

1
Принятый ответ необходимо изменить
Брайан Вебстер,

Ответы:


178

Благодаря современному C # 5/6 :)

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

public void bar()
{
    // do stuff
}

15
Этот ответ хорош по двум причинам. Простота кода и тот факт, что Delay НЕ создает поток и не использует пул потоков, как другие Task.Run или Task.StartNew ... это внутренне таймер.
Zyo 02

Приличное решение.
x4h1d 06

6
Также обратите внимание на немного более чистую (IMO) эквивалентную версию: Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Таран

5
@Zyo На самом деле он использует другой поток. Попробуйте получить доступ к элементу пользовательского интерфейса из него, и он вызовет исключение.
TudorT

@TudorT - Если Зио прав, что он работает в уже существующем потоке, который запускает события таймера, то его точка зрения состоит в том, что он не потребляет дополнительных ресурсов, создавая новый поток, и не ставит очередь в пул потоков. (Хотя я не знаю, значительно ли дешевле создание таймера, чем постановка задачи в очередь пула потоков - что ТАКЖЕ не создает поток, в этом вся суть пула потоков.)
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
}

(спасибо Fred Deschenes за идею размещения таймера в обратном вызове)


3
Я считаю, что это лучший ответ, чтобы отложить вызов функции. Ни потока, ни фоновой работы, ни сна. Таймер очень эффективен и разумен по памяти / процессору.
Zyo

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

Когда нужно утилизировать таймер?
Дидье А.

1
Здесь восстанавливается старый поток, но таймер можно расположить следующим образом: public static void CallWithDelay (Action method, int delay) {Timer timer = null; var cb = new TimerCallback ((состояние) => {метод (); timer.Dispose ();}); timer = новый таймер (cb, null, delay, Timeout.Infinite); } РЕДАКТИРОВАТЬ: Похоже, мы не можем публиковать код в комментариях ... VisualStudio должен правильно отформатировать его, когда вы все равно копируете / вставляете: P
Fred Deschenes

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.