97

したがって、私のアプリは、アプリが実行されているか、キャンセルが要求されている限り、ほぼ連続して (実行ごとに 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() を呼び出してキャンセルしますか?

4

3 に答える 3

97

これには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は、スレッド プールである a と同じです。ActionBlock<TInput>を呼び出してアクションを処理するために を使用Task.Delayすることで、実際には何もしていないときに使用していたスレッドの制御を譲ることになります。確かに、これは継続を処理する new を生成するときに実際にはいくらかのオーバーヘッドにつながりますTaskが、これをタイトなループで処理していないことを考えると、それは小さいはずです (呼び出しの間に 10 秒待機しています)。

関数を実際に awaitable にすることができる場合DoWork(つまり、 a を返すという点で)、(おそらく) 上記のファクトリ メソッドを微調整して、 の代わりにa を取得することで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);
}
于 2012-12-04T21:54:58.190 に答える
79

新しい 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.Delayoverを使用Thread.Sleepすることは、スリープ中に何もしないスレッドを拘束しないことを意味します。

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

于 2012-12-04T03:33:55.580 に答える
6

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

  • やりたい作業でメソッドを継承しNeverEndingTaskてオーバーライドします。ExecutionCore
  • 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();
    }
}
于 2016-06-07T00:29:15.800 に答える