LoginSignup
inf102
@inf102

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

C# ウェイト

Q&AClosed

下記のコード(WPF)の場合は Sleepが無いので毎秒2000位カウントアップされますが、
毎秒500カウントのアップにするにはどうすれば良いでしょうか。

Thread.Sleep(1); や、 await Task.Delay(1)では遅すぎます。
(毎秒100カウント程度になってしまう)

await Task.Delay(0.01); みたいな事ができれば良いのですが。
SpinWait.SpinUntil()も使用してみましたが遅すぎました。

private   void Window_Loaded(object sender, RoutedEventArgs e) {
    _= Task.Run (()=>Test());
}

void  Test() {
    int a=0;
            
    while (true){
        //   Thread.Sleep(1);
    
        this.Dispatcher.Invoke((Action)(() =>{
            Title=a.ToString();
    
            a++;
        }));

     }
}
0

3Answer

こまごまとした説明は省略しますが、Windowsの標準状態でのタイマー精度は15~16msec程度なので、Sleep(1)で1msecきっちりスリープ出来ると思ってはいけません。

timeBeginPeriod APIを呼び出すと精度を1msecまで改善する事が可能ですが、スレッドスケジューラに影響を与えるため、システム全体の負荷は上昇します。
あと、Sleepを全く挟まずに無限ループするとCPUリソースを使い果たす為、環境によっては熱暴走の危険もあります。

timeBeginPeriod(1) で精度を上げた後に、ループ毎に経過時間を計測し、その時点で経過していなければならない時間との差を取って、そのギャップ分Sleepする、みたいな事をすれば、500回に収める事が出来そうな気がします。

using System;
using System.Threading;
using System.Runtime.InteropServices;
using System.Diagnostics;

public class Program
{
    [DllImport("winmm.dll")]
    public static extern int timeBeginPeriod(int period);
    [DllImport("winmm.dll")]
    public static extern int timeEndPeriod(int period);

    private static void UnknownJob()
    {
        Thread.SpinWait(1);
    }

    public static void Main(string[] args)
    {
        const int maxCount = 500;

        // タイマー精度を1msecに上げる
        timeBeginPeriod(1);

        var sec = 0;
        var count = 0;
        var watch = Stopwatch.StartNew();
        while (sec < 10)
        {
            UnknownJob();

            // 1000/500なので、ループ毎に2msec経過しなければならない
            // 実際の経過時間とのギャップが1msec以上ある時、その分をSleep
            var elapsedMsec = watch.ElapsedMilliseconds;
            var diffTime = count * 2 - elapsedMsec;
            if (diffTime >= 1)
            {
                Thread.Sleep((int)diffTime);
            }

            count++;

            if (count == maxCount)
            {
                watch.Stop();
                Console.WriteLine($"経過時間:{watch.ElapsedMilliseconds} msec");
                count = 0;
                watch.Restart();
                sec++;
            }
        }

        // タイマー精度を戻す
        timeEndPeriod(1);
    }
}

大体こんな感じでしょうか。

3

Comments

  1. @inf102

    Questioner

    コードまでありがとうございます。
    この様なコードが必要なんですね。試してみます!。

  2. ちなみに、上記のサンプルソースはdiffTimeが常に1未満になり続けるくらい UnknownJob の処理が重い場合に全くSleep出来なくなるので、ループ回数を減らす、1未満でも最低1msはSleepさせる等よく考える必要があります。

  3. @inf102

    Questioner

    動きました。素晴らしいコード、どうもありがとうございました!。

  4. 解決されたようでよかったです。
    もし解決されたのであれば、質問のクローズをお願いします。

whlie ループが回る速さを 1 秒あたり 500 回にしたいと言ってますか? そもそも何でそんなことがやりたいのですか? XY 問題になってませんか?

0

whlie ループが回る速さを 1 秒あたり 500 回にしたいと言ってますか?
はい。その通りです。

2Dグラフィックスライブラリ SkiaSharpの描画用メソッドInvalidateVisual()の
呼びたしタイミングの調整です。
(SpinWait.SpinUntil()でいけると思ったんですが)

void Chile3Circle() {
// c はstatic変数
    while (true) {
        SpinWait.SpinUntil(()=> false, 1000 / 999);
        this.Dispatcher.Invoke((Action)(() =>{
            Child3Circle.InvalidateVisual();
            c-=1;
        }));
     }
}
0

Comments

  1. SkiaSharp とか触ったこともないので私の言ってることは見当違いかもしれませんが、よかったら教えてください。

    ググって調べると Child3Circle.InvalidateVisual(); は Child3Circle を再描画するということらしいのですが、そうなんでしょうか?

    再描画してユーザーに見せたいということだとすると、1 秒に 500 回も再描画しなくても、人間相手なら 30 回ぐらいで良さそうな気がしますが、違いますか?

  2. 標準的なディスプレイのリフレッシュレートが60~120Hz、高速なものでも240~360Hz程度ですから、まあ多いといえば多いですね。

  3. @inf102

    Questioner

    リフレッシュレートは関係ありません。
    InvalidateVisual()は一回呼ぶと1本線をひいたり、字を書いたりします。
    ぼくの記事で「ニコニコメッセージ風の表示アプリ」があるんですが、
    文字のX軸を可変させて移動させています。
    WQHDモニタ(横2560ドット)の場合は、毎秒2560回 InvalidateVisual()を呼ぶと1ドットづつ横に移動して1秒で端から端まで文字が移動します。
    毎秒500回の呼び出しにすれば当然、ゆっくり動いて約5秒かけて移動するという事になります。

  4. そこが本当に2560回必要かどうか、という話です。例えば、1ループで1ドットではなく32ドット動かせば80回で済む訳ですよね。
    ただ、問題は解決されたようですし、そのへんの話については質問の内容から逸れますから、あまり深入りしないでおきます。

  5. @inf102

    Questioner

    32ドット移動ではカクカク動作になります。

Your answer might help someone💌