System.Threading.Timerのタスクベースの置き換えはありますか?


88

.Net 4.0のタスクは初めてですが、定期的なタスクなど、タスクベースのタイマーの交換またはタイマーの実装だと思っていたものを見つけることができませんでした。そんなことありますか?

更新 私は自分のニーズに対するソリューションであると考えるものを思い付きました。それは、すべてのCancellationTokenを利用して子タスクで「タイマー」機能をタスク内にラップし、タスクを返して次のタスクステップに参加できるようにすることです。

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{ 
    Action wrapperAction = () =>
    {
        if (cancelToken.IsCancellationRequested) { return; }

        action();
    };

    Action mainAction = () =>
    {
        TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;

        if (cancelToken.IsCancellationRequested) { return; }

        if (delayInMilliseconds > 0)
            Thread.Sleep(delayInMilliseconds);

        while (true)
        {
            if (cancelToken.IsCancellationRequested) { break; }

            Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);

            if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }

            Thread.Sleep(intervalInMilliseconds);
        }
    };

    return Task.Factory.StartNew(mainAction, cancelToken);
}      

7
Thread.Sleepメカニズムを使用する代わりに、タスク内でタイマーを使用する必要があります。より効率的です。
ヨアン。B

回答:


84

4.5に依存しますが、これは機能します。

public class PeriodicTask
{
    public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(period, cancellationToken);

            if (!cancellationToken.IsCancellationRequested)
                action();
        }
     }

     public static Task Run(Action action, TimeSpan period)
     { 
         return Run(action, period, CancellationToken.None);
     }
}

もちろん、引数を取る汎用バージョンを追加することもできます。フードTask.Delayはタスクの完了ソースとしてタイマーの有効期限を使用しているため、これは実際には他の推奨アプローチと似ています。


1
たった今、このアプローチに切り替えました。しかし、私は条件付きaction()でを繰り返し呼び出します!cancelToken.IsCancellationRequested。それは良いですよね?
HappyNomad 2015

3
これをありがとう-私たちは同じものを使用していますが、アクションの後まで遅延を移動しました(アクションをすぐに呼び出してxの後に繰り返す必要があるため、より理にかなっています)
Michael Parker

1
これをありがとう。しかし、このコードは「X時間ごと」では実行されず、「X時間ごと+実行時間」でaction実行されますよね?
Alex

正しい。実行時間を考慮したい場合は、いくつかの計算が必要になります。ただし、実行時間が期間を超えた場合などは注意が必要です
Jeff

57

UPDATEは、 私は以下の回答をマーク、これは我々が非同期/のawaitパターンを使用する必要があることを今の古い十分にあるので、「答え」として。これに反対投票する必要はもうありません。笑


エイミーが答えたように、タスクベースの定期/タイマー実装はありません。ただし、元のUPDATEに基づいて、これを非常に便利なものに進化させ、実稼働テストを行いました。私が共有すると思いました:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task perdiodicTask = PeriodicTaskFactory.Start(() =>
            {
                Console.WriteLine(DateTime.Now);
            }, intervalInMilliseconds: 2000, // fire every two seconds...
               maxIterations: 10);           // for a total of 10 iterations...

            perdiodicTask.ContinueWith(_ =>
            {
                Console.WriteLine("Finished!");
            }).Wait();
        }
    }

    /// <summary>
    /// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
    /// </summary>
    public static class PeriodicTaskFactory
    {
        /// <summary>
        /// Starts the periodic task.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
        /// <param name="duration">The duration.
        /// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
        /// <returns>A <see cref="Task"/></returns>
        /// <remarks>
        /// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be 
        /// bubbled up to the periodic task.
        /// </remarks>
        public static Task Start(Action action,
                                 int intervalInMilliseconds = Timeout.Infinite,
                                 int delayInMilliseconds = 0,
                                 int duration = Timeout.Infinite,
                                 int maxIterations = -1,
                                 bool synchronous = false,
                                 CancellationToken cancelToken = new CancellationToken(),
                                 TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
        {
            Stopwatch stopWatch = new Stopwatch();
            Action wrapperAction = () =>
            {
                CheckIfCancelled(cancelToken);
                action();
            };

            Action mainAction = () =>
            {
                MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
            };

            return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        /// <summary>
        /// Mains the periodic task action.
        /// </summary>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="stopWatch">The stop watch.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="wrapperAction">The wrapper action.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
        private static void MainPeriodicTaskAction(int intervalInMilliseconds,
                                                   int delayInMilliseconds,
                                                   int duration,
                                                   int maxIterations,
                                                   CancellationToken cancelToken,
                                                   Stopwatch stopWatch,
                                                   bool synchronous,
                                                   Action wrapperAction,
                                                   TaskCreationOptions periodicTaskCreationOptions)
        {
            TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;

            CheckIfCancelled(cancelToken);

            if (delayInMilliseconds > 0)
            {
                Thread.Sleep(delayInMilliseconds);
            }

            if (maxIterations == 0) { return; }

            int iteration = 0;

            ////////////////////////////////////////////////////////////////////////////
            // using a ManualResetEventSlim as it is more efficient in small intervals.
            // In the case where longer intervals are used, it will automatically use 
            // a standard WaitHandle....
            // see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
            using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
            {
                ////////////////////////////////////////////////////////////
                // Main periodic logic. Basically loop through this block
                // executing the action
                while (true)
                {
                    CheckIfCancelled(cancelToken);

                    Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);

                    if (synchronous)
                    {
                        stopWatch.Start();
                        try
                        {
                            subTask.Wait(cancelToken);
                        }
                        catch { /* do not let an errant subtask to kill the periodic task...*/ }
                        stopWatch.Stop();
                    }

                    // use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
                    if (intervalInMilliseconds == Timeout.Infinite) { break; }

                    iteration++;

                    if (maxIterations > 0 && iteration >= maxIterations) { break; }

                    try
                    {
                        stopWatch.Start();
                        periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
                        stopWatch.Stop();
                    }
                    finally
                    {
                        periodResetEvent.Reset();
                    }

                    CheckIfCancelled(cancelToken);

                    if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
                }
            }
        }

        /// <summary>
        /// Checks if cancelled.
        /// </summary>
        /// <param name="cancelToken">The cancel token.</param>
        private static void CheckIfCancelled(CancellationToken cancellationToken)
        {
            if (cancellationToken == null)
                throw new ArgumentNullException("cancellationToken");

            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}

出力:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .

1
これはすばらしいコードのように見えますが、async / awaitキーワードがあるので、今必要なのかと思います。あなたのアプローチは、ここのスタックオーバーフロー.com / a / 14297203/122781とどのように比較しますか?
HappyNomad 2013年

1
@ HappyNomad、PeriodicTaskFactoryクラスが.Net 4.5をターゲットとするアプリケーションの非同期/待機を利用できるように見えますが、私たちにとっては、まだ.Net 4.5に移行できません。また、PeriodicTaskFactoryは、最大反復回数や最大継続時間などの追加の「タイマー」終了メカニズムを提供し、各反復が最後の反復で確実に待機できるようにする方法を提供します。しかし、これを.Net 4.5に移行するときに非同期/待機を使用するように適応させることを検討しています
Jim

4
+1私は今あなたのクラスを使っています、ありがとう。ただし、UIスレッドでうまく機能させるには、をTaskScheduler.FromCurrentSynchronizationContext()設定する前にを呼び出す必要がありますmainAction。次に、結果のスケジューラーを渡しMainPeriodicTaskActionsubTaskwith を作成します。
HappyNomad 2013年

2
よくわかりませんが、有用な作業ができるスレッドをブロックすることをお勧めします。「Thread.Sleep(delayInMilliseconds)」、「periodResetEvent.Wait(intervalInMilliseconds、cancelToken)」...次にタイマーを使用すると、ハードウェアで待機するため、スレッドが費やされることはありません。しかし、あなたのソリューションでは、スレッドは無駄に費やされています。
RollingStone 2017年

2
@rollingstone同意する。このソリューションは、非同期のような動作の目的に大きく反するものだと思います。タイマーを使用してスレッドを無駄にしない方がはるかに良いです。これは、非同期のように見えるだけで、メリットはありません。
ジェフ

12

正確にはにはありませんSystem.Threading.Tasksが、Reactive ExtensionsライブラリのObservable.Timer(またはより単純なObservable.Interval)がおそらくあなたが探しているものです。


1
例:Observable.Interval(TimeSpan.FromSeconds(1))。Subscribe(v => Debug.WriteLine(v));
Martin Capodici、2015年

1
いいですが、それらのReactiveコンストラクトは受け入れ可能ですか?
Shmil The Cat

9

これまでは、スレッドタイマーの代わりに周期的なCPUバインドのバックグラウンド処理にLongRunning TPLタスクを使用していました。

  • TPLタスクはキャンセルをサポートします
  • スレッド化タイマーは、プログラムのシャットダウン中に別のスレッドを開始し、破棄されたリソースで問題が発生する可能性があります
  • オーバーランの可能性:スレッド化タイマーは、予期しない長時間の作業が原因で前のスレッドがまだ処理されている間に別のスレッドを開始する可能性があります(タイマーを停止して再起動することで回避できます)

ただし、TPLソリューションは常に、次のアクションを待機している間(ほとんどの場合)には不要な専用スレッドを要求します。Jeffの提案されたソリューションを使用して、バックグラウンドでCPUバウンドの周期的な作業を実行したいと思います。スケーラビリティに優れた(特に間隔の期間が長い)作業がある場合にのみスレッドプールスレッドが必要になるためです。

それを達成するために、私は4つの適応を提案します:

  1. に追加ConfigureAwait(false)して、スレッドプールスレッドでアクションTask.Delay()を実行しdoWorkます。それ以外の場合doWorkは、並列処理の概念ではない呼び出しスレッドで実行されます。
  2. TaskCanceledExceptionをスローしてキャンセルパターンに固執します(まだ必要ですか?)
  3. CancellationTokenを転送しdoWorkてタスクをキャンセルできるようにする
  4. タイプオブジェクトのパラメーターを追加して、タスク状態情報(TPLタスクなど)を提供します。

ポイント2についてはわかりませんが、非同期待機にはTaskCanceledExecptionが必要ですか、それともベストプラクティスですか?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

提案されたソリューションにコメントを付けてください...

2016年8月30日更新

上記のソリューションはすぐには呼び出されませんdoWork()await Task.Delay().ConfigureAwait(false)、のスレッド切り替えを実現するために開始されdoWork()ます。以下の解決策は、最初のdoWork()呼び出しをaでラップしTask.Run()てそれを待つことで、この問題を克服しています。

以下は、Threading.Timerキャンセル可能な循環作業を実行し、スケーラブルな(TPLソリューションと比較して)次のアクションの待機中にスレッドを占有しないため、改善されたasync \ awaitの置き換えです。

タイマーとは異なり、待機時間(period)は一定であり、サイクルタイムではありません。サイクルタイムは、待機時間と継続時間の合計doWork()です。

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

を使用ConfigureAwait(false)すると、スレッドプールへのメソッドの継続がスケジュールされます。そのため、スレッドタイマーの2番目のポイントは実際には解決されません。私もtaskState必要だとは思いません。ラムダ変数のキャプチャはより柔軟でタイプセーフです。
Stephen Cleary

1
私が本当にやりたいことは交換することですawait Task.Delay()し、doWork()そうdoWork()すぐに起動時に実行されます。しかし、トリックdoWork()がないと、最初に呼び出しスレッドで実行され、ブロックされます。スティーブン、その問題の解決策はありますか?
Erik Stroeken

1
最も簡単な方法は、全体をでラップすることですTask.Run
Stephen Cleary

はい。ただし、ループが実行されている限りスレッドを要求し、このソリューションよりもスケーラビリティが低い限り、現在使用しているTPLソリューションに戻ることができます。
Erik Stroeken 16

1

同期メソッドから繰り返し非同期タスクをトリガーする必要がありました。

public static class PeriodicTask
{
    public static async Task Run(
        Func<Task> action,
        TimeSpan period,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        while (!cancellationToken.IsCancellationRequested)
        {

            Stopwatch stopwatch = Stopwatch.StartNew();

            if (!cancellationToken.IsCancellationRequested)
                await action();

            stopwatch.Stop();

            await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
        }
    }
}

これはジェフの答えの改作です。これはFunc<Task> 、次の遅延の期間からタスクの実行時間を差し引くことで、その期間が実行される頻度であることを確認するように変更されます。

class Program
{
    static void Main(string[] args)
    {
        PeriodicTask
            .Run(GetSomething, TimeSpan.FromSeconds(3))
            .GetAwaiter()
            .GetResult();
    }

    static async Task GetSomething()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine($"Hi {DateTime.UtcNow}");
    }
}


-1
static class Helper
{
    public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
    {
        while (worker.Worked)
        {
            execute();

            await Task.Delay(millisecond);
        }
    }
}


interface IWorker
{
    bool Worked { get; }
}

シンプル...

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