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