延迟函数调用

118
有没有一种简单的方法,可以延迟函数调用,同时让线程继续执行?
例如:
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完全实例化。重新阅读这个问题几乎让人感到糟糕的设计!

编辑

我会考虑这些关于糟糕设计的观点!我一直认为我可以改善系统,然而,这种令人不悦的情况仅当发生异常时才会发生,在所有其他时间里,这两个单例非常好地共存。我认为我不会拖延使用nasty async-patters,而是会重构其中一个类的初始化。


你需要修复它,但不要使用线程(或任何其他异步实践)。 - ShuggyCoUk
1
使用线程来同步对象初始化是需要采取其他方式的信号。Orchestrator似乎是更好的选择。 - thinkbeforecoding
1
复活!-- 在设计方面,你可以选择进行两阶段初始化。借鉴Unity3D API的经验,有AwakeStart两个阶段。在Awake阶段,你可以配置自己,在此阶段结束时所有对象都已初始化完成。在Start阶段,对象可以开始相互通信。 - cod3monk3y
1
需要更改已接受的答案。 - Brian Webster
12个回答

271
感谢现代化的C# 5/6技术 :)
public void foo()
{
    Task.Delay(1000).ContinueWith(t=> bar());
}

public void bar()
{
    // do stuff
}

21
这个答案很棒有两个原因。一是代码简洁,二是 Delay 不像其他的 Task.Run 或者 Task.StartNew 那样创建线程或使用线程池,它在内部实际上是一个计时器。 - Zyo
一个不错的解决方案。 - x4h1d
11
请注意一个稍微更简洁(我个人认为)的等效版本: Task.Delay(TimeSpan.FromSeconds(1)).ContinueWith(_ => bar()); (该代码段是需要翻译的内容) - Taran
9
实际上它确实使用了不同的线程。尝试从中访问UI元素,会触发异常。 - TudorT
@TudorT - 如果Zyo正确,它在已经存在的运行计时器事件的线程上运行,那么他的观点是它不会消耗额外的资源创建新线程,也不会排队到线程池中。(虽然我不知道创建计时器是否比将任务排队到线程池便宜得多 - 这也不会创建线程,这就是线程池的全部意义。) - ToolmakerSteve
4
以下是如何在UI线程上继续执行的代码:Task.Delay(1000).ContinueWith(_ => { Application.Current.Dispatcher.Invoke(() => { bar(); }); - Eternal21

107

我自己一直在寻找这样的东西 - 我想出了下面的方法,虽然它使用了一个定时器,但只用于初始延迟,并且不需要任何 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提供在回调函数中处理定时器的思路)


5
我认为这是延迟函数调用的最佳方法。没有线程、后台工作或者sleep。定时器在内存和CPU方面非常高效。 - Zyo
1
@Zyo,感谢您的评论 - 是的,定时器很有效,在许多情况下这种延迟非常有用,特别是在与某些无法控制的接口进行交互时 - 这些接口没有任何通知事件的支持。 - dodgy_coder
你什么时候释放计时器? - Didier A.
1
重新启动一个旧线程,但是计时器可以像这样处理: public static void CallWithDelay(Action method, int delay) { Timer timer = null; var cb = new TimerCallback((state) => { method(); timer.Dispose(); }); timer = new Timer(cb, null, delay, Timeout.Infinite); }编辑:好像我们不能在评论中发布代码...无论如何,当您复制/粘贴时,VisualStudio应该正确格式化它:P - Fred Deschenes
6
错误。在 lambda 中使用局部变量 timer 并将其绑定到委托对象 cb 会导致它被提升到一个匿名存储区(闭包实现细节),这将导致 Timer 对象从 GC 的角度而言在委托对象通过线程池调用之前都是可达的。换句话说,Timer 对象 保证 在委托对象被调用后不会被垃圾回收。 - cdhowie
显示剩余6条评论

17

除了同意之前评论者的设计观察之外,对我来说没有一个解决方案足够简洁。.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 ();
    }
}

为了解决用户双击造成的问题,我正在使用这个功能来防抖与UI元素上的左鼠标按钮弹起绑定的ICommand。(我知道我也可以使用Click/DoubleClick处理程序,但我想要一个适用于所有ICommand的解决方案。)

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

        _execute ();
    }
}

7

看起来需要在外部控制这两个对象的创建和它们之间的相互依赖关系,而不是在类之间进行控制。


+1,这听起来像是你需要某种编排器和工厂。 - ng5000

7

这确实是一个非常糟糕的设计,单例本身就是糟糕的设计。

然而,如果你真的需要延迟执行,以下是你可以做的:

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            
    }

3
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
MethodInvoker是一个Windows.Forms对象。请问Web开发人员有没有替代品?即:不会与System.Web.UI.WebControls冲突的东西。 - Fandango68

2

这将适用于旧版本的.NET
缺点:会在自己的线程中执行

class CancellableDelay
    {
        Thread delayTh;
        Action action;
        int ms;

        public static CancellableDelay StartAfter(int milliseconds, Action action)
        {
            CancellableDelay result = new CancellableDelay() { ms = milliseconds };
            result.action = action;
            result.delayTh = new Thread(result.Delay);
            result.delayTh.Start();
            return result;
        }

        private CancellableDelay() { }

        void Delay()
        {
            try
            {
                Thread.Sleep(ms);
                action.Invoke();
            }
            catch (ThreadAbortException)
            { }
        }

        public void Cancel() => delayTh.Abort();

    }

使用方法:

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

1

我认为完美的解决方案是使用计时器处理延迟操作。 FxCop 不喜欢间隔少于一秒的情况。 我需要延迟我的动作,直到我的 DataGrid 按列排序完成之后。 我想一个一次性计时器 (AutoReset = false) 就是解决方案,它完美地运行了。 此外, FxCop 不允许我忽略这个警告!


0

除了使用定时器和事件之外,没有标准的延迟调用函数的方法。

这听起来像是GUI反模式,即延迟调用方法以确保表单完成布局。这不是一个好主意。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接