関数呼び出しの遅延


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は、作成されているfooクラスのインスタンスを必要とするbarを呼び出す必要があるという問題があります。悪いデザインのほとんどスマック!

編集

悪いデザインについてはアドバイスをいただきながらポイントを取っていきます!私はシステムを改善できるかもしれないと長い間考えていましたが、この厄介な状況は例外がスローされたときにのみ発生し、それ以外の場合は2つのシングルトンが非常にうまく共存します。私は厄介な非同期パターンでメッセージを書くつもりはなく、クラスの1つの初期化をリファクタリングするつもりだと思います。


あなたはそれを修正する必要がありますが、スレッド(またはその問題のための他の非同期の練習)を使用することではありません
ShuggyCoUk

1
スレッドを使用してオブジェクトの初期化を同期する必要があることは、別の方法を取る必要があることを示しています。オーケストレーターはより良いオプションのようです。
thinkbeforecoding

1
復活!-デザインについてコメントすると、2段階の初期化を選択できます。Unity3D APIから描画するAwakeと、Startフェーズがあります。でAwake相、あなた自身を構成し、このフェーズの終わりまでにすべてのオブジェクトが初期化されます。Startフェーズ中、オブジェクトは相互に通信を開始できます。
cod3monk3y 2014年

1
承認された回答を変更する必要があります
ブライアンウェブスター

回答:


171

最新のC#5/6に感謝します:)

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

public void bar()
{
    // do stuff
}

14
この答えは2つの理由で素晴らしいです。コードの単純さと、Delayがスレッドを作成せず、他のTask.RunやTask.StartNewのようなスレッドプールを使用しないという事実は、内部的にはタイマーです。
Zyo

まともなソリューション。
x4h1d 2016年

5
また、少しクリーンな(IMO)同等バージョンにも注意してください。Task.Delay(TimeSpan.FromSeconds(1))。ContinueWith(_ => bar());
Taran、

4
@Zyo実際には別のスレッドを使用しています。そこからUI要素にアクセスしてみると、例外がトリガーされます。
TudorT 2018年

@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

1
@Zyo、コメントをありがとう-はい、タイマーは効率的であり、この種の遅延は多くの状況で役立ちます。特に、制御不能なものに接続する場合に役立ちます-通知イベントをサポートしていません。
dodgy_coder 2012

タイマーはいつ処分しますか?
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
@dodgy_coder不正解。timerデリゲートオブジェクトにバインドされるラムダ内からローカル変数を使用すると、デリゲート自体が到達可能である限りcbTimerオブジェクトがGCの観点から到達可能になるanonストア(クロージャー実装の詳細)に引き上げられます。TimerCallback。つまり、スレッドプールによってデリゲートオブジェクトが呼び出されるまで、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ハンドラーも使用できることを知っていますが、ICommand全面的にs で機能するソリューションが必要でした)。

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

        _execute ();
    }
}

7

これらのオブジェクトの作成の制御とそれらの相互依存性は、クラス間ではなく外部で制御する必要があるように思えます。


+1、これはある種のオーケスト
レータ

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ミリ秒ごとに実行しないように、ロジックをいくつか追加することをお勧めします。最初:最小許容間隔は1秒なので、遅延を500ミリ秒に増やすことができます。2つ目:新しいデリゲートが追加されたときにのみタイマーを開始し、デリゲートがなくなったときにタイマーを停止することができます。何もする必要がないときにCPUサイクルを使い続ける理由はありません。3番目に、タイマー間隔をすべてのデリゲート全体の最小遅延に設定できます。したがって、250ミリ秒ごとに起動するのではなく、デリゲートを呼び出す必要がある場合にのみ起動して、何かする必要があるかどうかを確認します。
Pic Mickael 2013

MethodInvokerはWindows.Formsオブジェクトです。Web開発者のための代替手段はありますか?つまり、System.Web.UI.WebControlsと競合しないものです。
Fandango68

1

完璧な解決策は、タイマーに遅延アクションを処理させることですが。FxCopは、間隔が1秒未満の場合は好ましくありません。DataGridが列による並べ替えを完了するまで、アクションを遅らせる必要があります。私はワンショットタイマー(AutoReset = false)が解決策になると考え、それは完全に機能しました。そして、FxCopは警告を抑制させません!


1

これは古いバージョンの.NETで動作します
短所:独自のスレッドで実行されます

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

デリゲートに一意のキーを使用することで、クラスを少し改善できます。最初のデリゲートが起動する前に同じデリゲートを2回追加すると、辞書に問題が発生する可能性があるためです。


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));
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.