web-dev-qa-db-ja.com

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

そのため、アプリが実行されているかキャンセルがリクエストされている限り、アプリはほぼ連続して(各実行の間に10秒程度のポーズで)アクションを実行する必要があります。必要な作業には、最大30秒かかる可能性があります。

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

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

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()を呼び出してキャンセルしますか?

83
Josh

このために 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;
}
_

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

また、デフォルトでは_ActionBlock<TInput>_は一度にoneアイテムのみを処理するため、1つのアクションのみが処理されることが保証されます(つまり、 tは、 reentrancy を処理する必要があります。 Post拡張メソッドを呼び出したとき

また、 CancellationToken構造体 を_ActionBlock<TInput>_のコンストラクターと の両方に渡しました。 _Task.Delay_ method call;プロセスがキャンセルされた場合、キャンセルは可能な限り最初の機会に行われます。

そこから、コードを簡単にリファクタリングして、 _ITargetBlock<DateTimeoffset>_ interface を_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);
}
_
92
casperOne

新しいTaskベースのインターフェイスは、このようなことを行うために非常にシンプルで、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.Delay_を_Thread.Sleep_経由で使用することは、スリープ中に何もしないスレッドを縛っていないことを意味します。

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

71
porges

ここに私が思いついたものがあります:

  • NeverEndingTaskから継承し、ExecutionCoreメソッドを目的の作業でオーバーライドします。
  • ExecutionLoopDelayMsを変更すると、ループ間の時間を調整できます。バックオフアルゴリズムを使用する場合。
  • Start/Stopタスクを開始/停止するための同期インターフェースを提供します。
  • LongRunningは、NeverEndingTaskごとに1つの専用スレッドを取得することを意味します。
  • このクラスは、上記の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();
    }
}
4
Schneider