そのため、アプリが実行されているかキャンセルがリクエストされている限り、アプリはほぼ連続して(各実行の間に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()を呼び出してキャンセルしますか?
このために 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);
}
_
新しい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()
がキャンセルトークンを受け入れるようにすることもでき、キャンセルの応答性が大幅に向上します。
ここに私が思いついたものがあります:
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();
}
}