延迟的函数调用


90

是否有一个很好的简单方法来延迟函数调用,同时让线程继续执行?

例如

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,而bar需要一个正在创建的foo类的实例...因此,延迟调用bar()以确保foo被完全实例化。差一点的不良设计!

编辑

我将在建议下处理有关不良设计的要点!长期以来,我一直以为我可以改善系统,但是,这种令人讨厌的情况只有在抛出异常时才会发生,在其他所有时间,两个单例都很好地共存。我认为我不会用讨厌的异步模式搞乱,而是要重构其中一个类的初始化。


您需要修复它,但不必使用线程(或任何其他异步方法)
ShuggyCoUk,2009年

1
必须使用线程来同步对象初始化是您应该采取另一种方式的信号。Orchestrator似乎是一个更好的选择。
thinkbeforecoding

1
复活!-评论设计,您可以选择进行两阶段初始化。从Unity3D API绘制,也有AwakeStart阶段。在此Awake阶段中,您需要配置自己,到此阶段结束时,所有对象都将初始化。在此Start阶段,对象可以开始彼此通信。
cod3monk3y 2014年

1
接受的答案需要更改
Brian Webster,

Answers:


171

多亏了现代的C#5/6 :)

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

public void bar()
{
    // do stuff
}

14
这个答案很棒,有两个原因。代码简单,并且Delay不创建线程,也不像其他Task.Run或Task.StartNew一样使用线程池,这一事实在内部是计时器。
Zyo

一个体面的解决方案。
x4h1d 16-10-6

5
还请注意一个稍微更清洁(IMO)的等效版本:Task.Delay(TimeSpan.FromSeconds(1))。ContinueWith(_ => bar());
塔兰

4
@Zyo实际上,它确实使用其他线程。尝试从中访问UI元素,它将触发异常。
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
}

(感谢Fred Deschenes提供了在回调中放置计时器的想法)


3
我认为这通常是延迟函数调用的最佳答案。没有线程,没有后台工作,没有睡眠。计时器非常高效,并且内存/ CPU明智。
Zyo 2012年

1
@Zyo,感谢您的评论-是的,计时器非常有效,这种延迟在许多情况下非常有用,尤其是在与您无法控制的内容进行接口连接时-不支持通知事件。
dodgy_coder 2012年

您何时处置计时器?
Didier A.

1
在这里恢复旧线程,但是可以按以下方式处理计时器:public static void CallWithDelay(Action method,int delay){计时器timer = null; var cb = new TimerCallback((state)=> {method(); timer.Dispose();}); 计时器=新的计时器(cb,null,延迟,超时。无限); }编辑:看来我们不能在注释中发布代码...无论如何,当您复制/粘贴它时,VisualStudio应该正确格式化它:P
Fred Deschenes 2013年

6
@dodgy_coder错误。timer在绑定到委托对象的lambda中使用局部变量cb会导致将其提升到匿名存储(关闭实现细节)上,Timer只要TimerCallback委托本身可访问,就可以从GC的角度访问该对象。。换句话说,该Timer目的是保证不被收集,直到委托对象由线程池调用后的垃圾。
cdhowie 2015年

15

除了同意先前评论者的设计观点之外,没有一种解决方案对我而言足够干净。.Net 4提供DispatcherTask类,这些类使延迟在当前线程上执行非常简单:

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消除UI元素上与鼠标左键相关的绑定。用户双击会造成各种破坏。(我知道我也可以使用Click/ DoubleClick处理器,但是我想要一个可以与ICommands 一起使用的解决方案)。

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

        _execute ();
    }
}

7

听起来像是要控制这两个对象的创建,并且它们的相互依赖性需要在外部而不是在类之间进行控制。


+1,这听起来好像您需要某种协调器,也许还需要一家工厂
ng5000 2009年

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秒。第二:您只能在添加新的委托时启动计时器,而在没有更多委托时停止计时器。无事可做时,没有理由继续使用CPU周期。第三:您可以将计时器间隔设置为所有代表之间的最小延迟。因此,它仅在需要调用委托时才唤醒,而不是每250毫秒唤醒一次以查看是否有事要做。
Pic Mickael 2013年

MethodInvoker是Windows.Forms对象。请问Web开发人员是否有其他选择?即:与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

除了使用计时器和事件外,没有其他方法可以延迟对函数的调用。

这听起来像GUI反模式,它延迟了对方法的调用,因此您可以确保表单已完成布局。这不是一个好主意。


0

根据David O'Donoghue的回答,这里是延迟代表的优化版本:

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));
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.