これには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);
}