てくメモ

trivial な notes

【C#】PeriodicTimer と Task.Delay

.NETにはタイマー的なものがたくさんある。

PeriodicTimerはそのなかでも非同期タイマーと呼ばれるもの。
WaitForNextTickAsyncメソッドを await する使い方ができる。

// IDisposable
using PeriodicTimer timer = new(TimeSpan.FromSeconds(3));
// CancellationToken 対応
CancellationTokenSource cts = new();

try
{
    // 3秒待機を挟みながら5回ループ
    int count = 0;
    do
    {
        Console.WriteLine(++count);

    } while (count < 5
        && await timer.WaitForNextTickAsync(cts.Token)); // 待機
}
catch(OperationCanceledException)
{
    Console.WriteLine("canceled");
}


……このタイマー、Task.Delayに似てるような?

using PeriodicTimer timer = new(TimeSpan.FromSeconds(3));
await timer.WaitForNextTickAsync(cts.Token);
// ↑↓ (?)
await Task.Delay(TimeSpan.FromSeconds(3), cts.Token);


他にAPIがあったりするかと思えば、タイマーとしてはWaitForNextTickAsyncがすべてなスタイル。
Dispose()可能だけれど、中断はTask.Delayもキャンセルというかたちでサポートしている。

あんまり違わない、のだろうか?


……というのは、自分の浅はかな第一印象。
実際は次のような使い分けがあるらしい。

このAPIは、繰り返し起動するタイマにのみ意味があり、一度だけ起動するタイマはTaskベースになるでしょう。(このために、既にTask.Delayがあります。)

.NET 6: スレッドの改善 より)


なるほど、タイマーは複数回、Task.Delayは基本的に一度。
数字的な違いはあるのか、試しに60ミリ秒間隔と1秒間隔の10回ループをBenchmarkDotNetにて較べてみる。

比較用コード折りたたみ

[Params(60d, 1000d)]
public double Milliseconds;

public TimeSpan TimeSpan;

[GlobalSetup]
public void Setup()
{
    TimeSpan = TimeSpan.FromMilliseconds(Milliseconds);
}

[Benchmark]
public async Task TaskDelay()
{
    CancellationTokenSource cts = new();
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(TimeSpan, cts.Token);
    }
}

[Benchmark]
public async Task PeriodicTimerWait()
{
    using PeriodicTimer timer = new(TimeSpan);
    CancellationTokenSource cts = new();
    for (int i = 0; i < 10; i++)
    {
        await timer.WaitForNextTickAsync(cts.Token);
    }
}

            Method | Milliseconds |        Mean |     Error |   StdDev | Allocated |
------------------ |------------- |------------:|----------:|---------:|----------:|
         TaskDelay |           60 |    622.5 ms |  34.44 ms |  1.89 ms |   3.12 KB |
 PeriodicTimerWait |           60 |    606.9 ms |  39.60 ms |  2.17 ms |   1.48 KB |
         TaskDelay |         1000 | 10,140.4 ms | 232.67 ms | 12.75 ms |   3.12 KB |
 PeriodicTimerWait |         1000 | 10,008.6 ms |  91.02 ms |  4.99 ms |   1.48 KB |


まず、アロケーションPeriodicTimerの方が少ない。
そして、Mean が期待される時間に近い。


複数回 await する用途ならPeriodicTimerが良いと確認。