async/awaitにおけるミリ秒単位の待機方法

C#で一定間隔で処理を実行したい場合の待機方法。

まず基本

await Task.Delay(TimeSpan.FromSeconds(3));

私は最近、ある程度長い時間(100ms以上)待機するときは、TimeSpan使っています。
100msを超えるような間隔の場合、特に問題ないのですが、これが5ms間隔とかになってくると問題が起きます。

await Task.Delay(5); // 5ms

これを実行すると、15msほど待機してしまいます。
これは、ドキュメントによると仕様とのことです。

ちょっと寄り道して・・・

var results = new List<DateTime>(10000);

for(int i = 0;i<100;i++)
{
    var nextTime = DateTime.UtcNow.AddMilliseconds(5);
    while(nextTime > DateTime.UtcNow)
    {
        await Task.Yield();
    }
    results.Add(DateTime.UtcNow);

    await Task.Delay(1);
    results.Add(DateTime.UtcNow);
}

こんなコードを書いてテストしてみました。
この場合、Task.Delayで待つ時間は10msほどになりました。
Task.Delayから15msというわけではなくて、15ms間隔でタイマーが動いていて、次のタイマーまで待つという挙動をするようですね。

このテストコードで書いたように、目的の時間になるまで、Task.Yeild()をしながら待つコードにすると、
自分のプロセスの他の非同期のタスクを止めずに、目的の時間を待つことができます。待つ時間は、マイクロ秒とかでも対応できます。
ただ、このプログラム、待っている間ずっとCPUが動いているので、待っている間、CPUが100%になってしまう問題があります。

Thred.Sleepを使って待つこともできますが、この場合はCPU負荷は問題になりませんが、自プロセスの他のタスクがある場合、その動作が止まってしまう問題があります。
(2つ以上のスレッドがあれば、他のスレッドは動くでしょうが)

さて、上記を踏まえて、今のところの最終案

var waitTime = _nextTime - DateTime.UtcNow - TimeSpan.FromMilliseconds(15);
if (waitTime > TimeSpan.Zero) { await Task.Delay(waitTime).ConfigureAwait(false); }

while (true)
{
    waitTime = _nextTime - DateTime.UtcNow;

    if (waitTime <= TimeSpan.Zero) { break; }
    await Task.Yield();
}

15ms少なくTask.Delayで待って、最後の部分は、Task.Yieldのループで待つ。
Task.Yieldループで待つ部分は、CPU100%を許容。
間隔が15ms以内の場合は、ずっとCPUが100%になっちゃうね。うーん。困った。

今のところ、CPU負荷を高くせずに、数ミリ秒,マイクロ秒の待ちを達成する方法が見つかっていません。
どなたがご存じの方がおりましたら、コメントなどで教えていただけると助かります。

投稿日:
カテゴリー: C#

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です