この質問には、TPL のいくつかの機能に基づいた少し新しい回答を含め、すでに多くの適切な回答があります。しかし、私はここに不足を感じています:
- TPLベースのソリューションは、a)ここに完全に含まれているわけではなく、別の回答を参照しています.b)単一のメソッドでタイミングメカニズムを実装するために
async
/を使用する方法を示していません.c)参照されている実装はかなり複雑で、この特定の質問await
の根底にある関連点がややわかりにくくなっています。
- ここでの元の質問は、目的の実装の特定のパラメーターについてややあいまいです (ただし、その一部はコメントで明確にされています)。同時に、他の読者も似たようなニーズを持っているかもしれませんが、同じではないかもしれません。
- コードを簡素化する方法のため、
Task
and async
/この方法を使用して定期的な動作を実装するのが特に好きです。await
特にasync
/await
機能は、そうでなければ継続/コールバック実装の詳細によって分割されるコードを取得し、その自然で線形のロジックを単一のメソッドで保持するのに非常に役立ちます。しかし、その単純さを示す答えはここにはありません。
それで、その理論的根拠により、この質問にさらに別の答えを追加する動機が生まれました…
私にとって、最初に考慮すべきことは、「ここで正確にどのような動作が必要か?」ということです。ここでの質問は基本的な前提から始まります。タスクがタイマー期間よりも長くかかる場合でも、タイマーによって開始された期間タスクは同時に実行されるべきではないということです。ただし、次のような前提を満たす方法は複数あります。
- タスクの実行中にタイマーを実行しないでください。
- タイマーを実行します (これと、ここで提示する残りのオプションはすべて、タスクの実行中にタイマーが引き続き実行されることを前提としています)。前のタイマーティック。
- タイマーティックでのみタスクの実行を開始します。タスクがタイマー期間よりも長くかかる場合は、現在のタスクの実行中に新しいタスクを開始しないでください。また、現在のタスクが完了した後でも、次のタイマー ティックまで新しいタスクを開始しないでください。
- タスクがタイマー間隔よりも長くかかる場合は、タスクが完了した直後にタスクを再実行するだけでなく、タスクが「追いつく」まで必要な回数だけ実行します。つまり、時間の経過とともに、タイマー ティックごとに1 回タスクを実行するように最善を尽くします。
コメントに基づいて、#3 オプションが OP の元の要求に最もよく一致するという印象がありますが、#1 オプションも機能する可能性があるようです。しかし、オプション #2 と #4 は他の誰かよりも好ましいかもしれません。
次のコード例では、これらのオプションを 5 つの異なる方法で実装しています (そのうちの 2 つはオプション #3 を実装していますが、方法が少し異なります)。もちろん、必要に応じて適切な実装を選択します。1 つのプログラムで 5 つすべてが必要なわけではありません。:)
重要な点は、これらの実装のすべてにおいて、自然にかつ非常に単純な方法で、タスクをピリオドではあるが同時実行ではない方法で実行することです。つまり、タイマーベースの実行モデルを効果的に実装しながら、質問の主要な要求ごとに、タスクが一度に 1 つのスレッドによってのみ実行されるようにします。
この例ではCancellationTokenSource
、 を使用して period タスクを中断し、 を利用しawait
て例外ベースのモデルをクリーンでシンプルな方法で処理する方法も示しています。
class Program
{
const int timerSeconds = 5, actionMinSeconds = 1, actionMaxSeconds = 7;
static Random _rnd = new Random();
static void Main(string[] args)
{
Console.WriteLine("Press any key to interrupt timer and exit...");
Console.WriteLine();
CancellationTokenSource cancelSource = new CancellationTokenSource();
new Thread(() => CancelOnInput(cancelSource)).Start();
Console.WriteLine(
"Starting at {0:HH:mm:ss.f}, timer interval is {1} seconds",
DateTime.Now, timerSeconds);
Console.WriteLine();
Console.WriteLine();
// NOTE: the call to Wait() is for the purpose of this
// specific demonstration in a console program. One does
// not normally use a blocking wait like this for asynchronous
// operations.
// Specify the specific implementation to test by providing the method
// name as the second argument.
RunTimer(cancelSource.Token, M1).Wait();
}
static async Task RunTimer(
CancellationToken cancelToken, Func<Action, TimeSpan, Task> timerMethod)
{
Console.WriteLine("Testing method {0}()", timerMethod.Method.Name);
Console.WriteLine();
try
{
await timerMethod(() =>
{
cancelToken.ThrowIfCancellationRequested();
DummyAction();
}, TimeSpan.FromSeconds(timerSeconds));
}
catch (OperationCanceledException)
{
Console.WriteLine();
Console.WriteLine("Operation cancelled");
}
}
static void CancelOnInput(CancellationTokenSource cancelSource)
{
Console.ReadKey();
cancelSource.Cancel();
}
static void DummyAction()
{
int duration = _rnd.Next(actionMinSeconds, actionMaxSeconds + 1);
Console.WriteLine("dummy action: {0} seconds", duration);
Console.Write(" start: {0:HH:mm:ss.f}", DateTime.Now);
Thread.Sleep(TimeSpan.FromSeconds(duration));
Console.WriteLine(" - end: {0:HH:mm:ss.f}", DateTime.Now);
}
static async Task M1(Action taskAction, TimeSpan timer)
{
// Most basic: always wait specified duration between
// each execution of taskAction
while (true)
{
await Task.Delay(timer);
await Task.Run(() => taskAction());
}
}
static async Task M2(Action taskAction, TimeSpan timer)
{
// Simple: wait for specified interval, minus the duration of
// the execution of taskAction. Run taskAction immediately if
// the previous execution too longer than timer.
TimeSpan remainingDelay = timer;
while (true)
{
if (remainingDelay > TimeSpan.Zero)
{
await Task.Delay(remainingDelay);
}
Stopwatch sw = Stopwatch.StartNew();
await Task.Run(() => taskAction());
remainingDelay = timer - sw.Elapsed;
}
}
static async Task M3a(Action taskAction, TimeSpan timer)
{
// More complicated: only start action on time intervals that
// are multiples of the specified timer interval. If execution
// of taskAction takes longer than the specified timer interval,
// wait until next multiple.
// NOTE: this implementation may drift over time relative to the
// initial start time, as it considers only the time for the executed
// action and there is a small amount of overhead in the loop. See
// M3b() for an implementation that always executes on multiples of
// the interval relative to the original start time.
TimeSpan remainingDelay = timer;
while (true)
{
await Task.Delay(remainingDelay);
Stopwatch sw = Stopwatch.StartNew();
await Task.Run(() => taskAction());
long remainder = sw.Elapsed.Ticks % timer.Ticks;
remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
}
}
static async Task M3b(Action taskAction, TimeSpan timer)
{
// More complicated: only start action on time intervals that
// are multiples of the specified timer interval. If execution
// of taskAction takes longer than the specified timer interval,
// wait until next multiple.
// NOTE: this implementation computes the intervals based on the
// original start time of the loop, and thus will not drift over
// time (not counting any drift that exists in the computer's clock
// itself).
TimeSpan remainingDelay = timer;
Stopwatch swTotal = Stopwatch.StartNew();
while (true)
{
await Task.Delay(remainingDelay);
await Task.Run(() => taskAction());
long remainder = swTotal.Elapsed.Ticks % timer.Ticks;
remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
}
}
static async Task M4(Action taskAction, TimeSpan timer)
{
// More complicated: this implementation is very different from
// the others, in that while each execution of the task action
// is serialized, they are effectively queued. In all of the others,
// if the task is executing when a timer tick would have happened,
// the execution for that tick is simply ignored. But here, each time
// the timer would have ticked, the task action will be executed.
//
// If the task action takes longer than the timer for an extended
// period of time, it will repeatedly execute. If and when it
// "catches up" (which it can do only if it then eventually
// executes more quickly than the timer period for some number
// of iterations), it reverts to the "execute on a fixed
// interval" behavior.
TimeSpan nextTick = timer;
Stopwatch swTotal = Stopwatch.StartNew();
while (true)
{
TimeSpan remainingDelay = nextTick - swTotal.Elapsed;
if (remainingDelay > TimeSpan.Zero)
{
await Task.Delay(remainingDelay);
}
await Task.Run(() => taskAction());
nextTick += timer;
}
}
}
最後に 1 つ: 別の質問の複製としてこの Q&A をたどった後、この Q&A に出会いました。その他の質問では、こことは異なり、OP はSystem.Windows.Forms.Timer
クラスを使用していることを特に指摘していました。もちろん、このクラスが主に使用されるのはTick
、UI スレッドでイベントが発生するという優れた機能があるためです。
さて、it と this の両方の質問には、バックグラウンド スレッドで実際に実行されるタスクが含まれているため、そのタイマー クラスの UI スレッド アフィニティ動作は、これらのシナリオでは特に使用されません。ここでのコードは、「バックグラウンド タスクを開始する」というパラダイムに一致するように実装されていますが、taskAction
デリゲートが実行されて待機するのではなく、単に直接呼び出されるように簡単に変更できますTask
。上記の構造上の利点に加えて、async
/を使用する利点は、クラスに望ましいスレッド アフィニティの動作が維持されることです。await
System.Windows.Forms.Timer