終わりのないタスクを実装するための適切な方法。(タイマーvsタスク)


92

そのため、私のアプリは、アプリが実行されているか、キャンセルが要求されている限り、ほぼ連続的に(実行ごとに10秒程度の休止を置いて)アクションを実行する必要があります。必要な作業には、最大30秒かかる可能性があります。

System.Timers.Timerを使用し、AutoResetを使用して、前の「ティック」が完了する前にアクションが実行されないようにすることをお勧めします。

または、キャンセルトークンを使用してLongRunningモードで一般的なタスクを使用し、呼び出しの間に10秒間のThread.Sleepを使用してアクションを実行するアクションを呼び出す通常の無限whileループを含める必要がありますか?非同期/待機モデルについては、作業からの戻り値がないため、ここが適切かどうかはわかりません。

CancellationTokenSource wtoken;
Task task;

void StopWork()
{
    wtoken.Cancel();

    try 
    {
        task.Wait();
    } catch(AggregateException) { }
}

void StartWork()
{
    wtoken = new CancellationTokenSource();

    task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            wtoken.Token.ThrowIfCancellationRequested();
            DoWork();
            Thread.Sleep(10000);
        }
    }, wtoken, TaskCreationOptions.LongRunning);
}

void DoWork()
{
    // Some work that takes up to 30 seconds but isn't returning anything.
}

または、AutoResetプロパティの使用中に単純なタイマーを使用し、.Stop()を呼び出してキャンセルしますか?


達成しようとしていることを考えると、タスクはやりすぎのようです。en.wikipedia.org/wiki/KISS_principle。OnTick()の開始時にタイマーを停止し、ブール値をチェックして、実行していないことを確認し、作業を行い、完了したらタイマーを再起動します。
Mike Trusov

回答:


94

これにはTPL Dataflowを使用します(.NET 4.5を使用していて、Task内部で使用しているため)。ActionBlock<TInput>アクションを処理して適切な時間待機した後、アイテムを自分自身に投稿するを簡単に作成できます。

まず、終わりのないタスクを作成するファクトリを作成します。

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.
        action(now);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

私は構造ActionBlock<TInput>を取るために選択しました。型パラメーターを渡す必要があり、便利な状態を渡すこともできます(必要に応じて、状態の性質を変更できます)。DateTimeOffset

また、ではActionBlock<TInput>デフォルトで一度に1つのアイテムしか処理されないため、処理されるアクションは1つだけであることが保証されます(つまり、拡張メソッド自体を呼び出すときに再入可能性を処理する必要はありません)。Post

また、私は渡されてきたCancellationToken構造のコンストラクタの両方にActionBlock<TInput>とのTask.Delayメソッドの呼び出しを、プロセスがキャンセルされた場合、キャンセルは可能な限り最初の機会に行われます。

そこから、コードを簡単にリファクタリングして、によって実装されたITargetBlock<DateTimeoffset>インターフェースを格納しますActionBlock<TInput>(これは、コンシューマーであるブロックを表す高レベルの抽象化であり、Post拡張メソッドの呼び出しを通じて消費をトリガーできるようにしたい)。

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

あなたのStartWork方法:

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now);
}

そしてあなたのStopWork方法:

void StopWork()
{
    // CancellationTokenSource implements IDisposable.
    using (wtoken)
    {
        // Cancel.  This will cancel the task.
        wtoken.Cancel();
    }

    // Set everything to null, since the references
    // are on the class level and keeping them around
    // is holding onto invalid state.
    wtoken = null;
    task = null;
}

ここでTPL Dataflowを使用する理由は何ですか?いくつかの理由:

関心事の分離

CreateNeverEndingTaskこの方法は現在、いわばあなたの「サービス」を作成するファクトリです。起動と停止のタイミングはユーザーが制御し、完全に自己完結型です。タイマーの状態制御をコードの他の側面と織り交ぜる必要はありません。単にブロックを作成して開始し、完了したら停止します。

スレッド/タスク/リソースのより効率的な使用

TPLデータフローのブロックのデフォルトスケジューラTaskは、スレッドプールであるの場合と同じです。を使用しActionBlock<TInput>てアクションを処理し、を呼び出すことでTask.Delay、実際には何も実行していないときに使用していたスレッドを制御できます。確かに、これTaskは継続を処理する新しいものを生成するときに実際にいくつかのオーバーヘッドにつながりますが、タイトなループでこれを処理していないことを考えると、それは小さいはずです(呼び出しの間に10秒待機しています)。

DoWork関数が実際に待機可能にできる場合(つまり、が返される場合Task)、上記のファクトリメソッドを調整してのFunc<DateTimeOffset, CancellationToken, Task>代わりにを取得することで、(おそらく)次のようにさらに最適化できますAction<DateTimeOffset>

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.  Wait on the result.
        await action(now, cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Same as above.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

もちろん、CancellationTokenここで行われているように、メソッドにスルーを織り込むことをお勧めします(それが受け入れられる場合)。

つまりDoWorkAsync、次のシグネチャを持つメソッドを持つことになります。

Task DoWorkAsync(CancellationToken cancellationToken);

次のようStartWorkに、CreateNeverEndingTaskメソッドに渡された新しい署名を説明するために、メソッドを変更する必要があります(ほんの少しだけ、そしてここでの懸念の分離を実現していません)。

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now, wtoken.Token);
}

こんにちは、私はこの実装を試みていますが、問題に直面しています。DoWorkが引数を取らない場合、task = CreateNeverEndingTask(now => DoWork()、wtoken.Token); ビルドエラーが発生します(型の不一致)。一方、私のDoWorkがDateTimeOffsetパラメーターを受け取った場合、その同じ行で異なるビルドエラーが発生し、DoWorkのオーバーロードは引数を取らないことがわかります。これを理解するのを手伝っていただけませんか?
Bovaz 2014

1
実際、タスクを割り当てる行にキャストを追加し、パラメーターをDoWorkに渡すことで問題を解決しました。task =(ActionBlock <DateTimeOffset>)CreateNeverEndingTask(now => DoWork(now)、wtoken.Token);
Bovaz 14

「ActionBlock <DateTimeOffset>タスク」のタイプを変更することもできます。ITargetBlock <DateTimeOffset>タスクへ。
XOR

1
私はこれがメモリを永久に割り当てる可能性が高いと信じており、最終的にはオーバーフローにつながります。
Nate Gardner

@NateGardnerどの部分ですか?
casperOne 2018

75

新しいタスクベースのインターフェイスは、このようなことを実行するために非常に単純であることがわかります。Timerクラスを使用するよりもさらに簡単です。

あなたの例に行うことができるいくつかの小さな調整があります。の代わりに:

task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        wtoken.Token.ThrowIfCancellationRequested();
        DoWork();
        Thread.Sleep(10000);
    }
}, wtoken, TaskCreationOptions.LongRunning);

あなたはこれを行うことができます:

task = Task.Run(async () =>  // <- marked async
{
    while (true)
    {
        DoWork();
        await Task.Delay(10000, wtoken.Token); // <- await with cancellation
    }
}, wtoken.Token);

このようにすると、キャンセルは内で瞬時に行わTask.Delayれ、のThread.Sleep終了を待つ必要はありません。

また、Task.Delayover を使用Thread.Sleepすることは、スリープの間、何もしないスレッドを拘束しないことを意味します。

可能であればDoWork()、キャンセルトークンを受け入れるようにすることもでき、キャンセルの応答性が大幅に向上します。


1
あなたがTask.Factory.StartNewのパラメータとして非同期ラムダを使用する場合は得るものタスクアウトWhatch - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspxあなたがtask.Waitを行うと( ); キャンセルが要求された後、あなたは間違ったタスクを待つことになります。
Lukas Pirkl 14

はい、これは実際には正しいオーバーロードを持つTask.Runになります。
ポルジェス2015年

http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspxによるとTask.Run、スレッドプールを使用しているように見えるため、withのTask.Run代わりに使用した例では、まったく同じことは行わTask.Factory.StartNewTaskCreationOptions.LongRunningません。 - LongRunningオプションを使用するタスクが必要な場合、Task.Run表示されているように使用できませんか、それとも何か不足していますか?
Jeff

@Lumirris:async / awaitのポイントは、実行中ずっとスレッドが占有されるのを避けることです(ここでは、Delay呼び出しの間、タスクはスレッドを使用していません)。したがって、使用LongRunningは、スレッドを拘束しないという目的とは少し互換性がありません。独自のスレッドでの実行を保証したい場合は、それを使用できますが、ここではほとんどの時間スリープしているスレッドを開始します。ユースケースは何ですか?
ポルジュ2015年

@Porgesポイントを取得。私の使用例は、無限ループを実行するタスクであり、各反復は作業のチャンクを実行し、次の反復で別の作業を行う前に2秒間「リラックス」します。それは永遠に実行されていますが、定期的に2秒の休憩を取ります。ただし、私のコメントはLongRunningTask.Run構文を使用して指定できるかどうかについての詳細です。ドキュメントから、Task.Runそれが使用するデフォルト設定に満足している限り、構文はよりすっきりしているように見えます。TaskCreationOptions引数を取るオーバーロードはありません。
Jeff

4

これが私が思いついたものです:

  • メソッドを継承しNeverEndingTaskExecutionCore実行する作業でメソッドをオーバーライドします。
  • 変更ExecutionLoopDelayMsすることで、たとえばバックオフアルゴリズムを使用する場合など、ループ間の時間を調整できます。
  • Start/Stop タスクを開始/停止するための同期インターフェースを提供します。
  • LongRunningは、ごとに1つの専用スレッドを取得することを意味しますNeverEndingTask
  • このクラスは、ActionBlock上記のベースのソリューションとは異なり、ループでメモリを割り当てません。
  • 以下のコードはスケッチであり、必ずしも製品コードではありません:)

public abstract class NeverEndingTask
{
    // Using a CTS allows NeverEndingTask to "cancel itself"
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    protected NeverEndingTask()
    {
         TheNeverEndingTask = new Task(
            () =>
            {
                // Wait to see if we get cancelled...
                while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs))
                {
                    // Otherwise execute our code...
                    ExecutionCore(_cts.Token);
                }
                // If we were cancelled, use the idiomatic way to terminate task
                _cts.Token.ThrowIfCancellationRequested();
            },
            _cts.Token,
            TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning);

        // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable
        TheNeverEndingTask.ContinueWith(x =>
        {
            Trace.TraceError(x.Exception.InnerException.Message);
            // Log/Fire Events etc.
        }, TaskContinuationOptions.OnlyOnFaulted);

    }

    protected readonly int ExecutionLoopDelayMs = 0;
    protected Task TheNeverEndingTask;

    public void Start()
    {
       // Should throw if you try to start twice...
       TheNeverEndingTask.Start();
    }

    protected abstract void ExecutionCore(CancellationToken cancellationToken);

    public void Stop()
    {
        // This code should be reentrant...
        _cts.Cancel();
        TheNeverEndingTask.Wait();
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.