1

新しいタイマーを作成し、ティックごとに (HTTP 経由で) リモート キューをポーリングするクラスがあります。

public void Start()
{
    _timer = new Timer((x) =>
    {
        Console.WriteLine(DateTime.Now.ToString("hh:MM:ss.fff") + " " + typeof(T).Name);                

        var message = (Message)null;
        var messageBody = (T)null;

        try
        {
            if (!_queue.TryGet(out message))
                return;

                messageBody = (T)JsonConvert.DeserializeObject(message.Body, typeof(T));

                _messageDispatcher.Dispatch<T>(messageBody);

                _queue.Delete(message.Id);
            }
            catch (Exception ex)
            {
                _errorHandler.Handle(ex, message);
            }                
        }, null, 0, _queueConsumerConfiguration.PollingInterval);            
    }
}

このクラスの 8 つの新しいインスタンスを作成し、ポーリング間隔を 250 ミリ秒に設定してそれらを呼び出した場合、タイマーはかなり正確に作動すると思います。タイマー コールバック内で実行される内容は重要ではありません。しかし、そうではありません。

01:03:23.305 MessageSleepForOneSecond
01:03:23.301 MessageSleepForOneSecond
01:03:23.297 MessageSleepForOneSecond
01:03:23.316 MessageSleepForOneSecond
01:03:24.321 MessageSleepForOneSecond
01:03:24.562 MessageSleepForOneSecond
01:03:24.701 MessageSleepForOneSecond
01:03:24.707 MessageSleepForOneSecond
01:03:24.716 MessageSleepForOneSecond
01:03:25.321 MessageSleepForOneSecond
01:03:25.518 MessageSleepForOneSecond
01:03:25.764 MessageSleepForOneSecond
01:03:25.912 MessageSleepForOneSecond
01:03:25.920 MessageSleepForOneSecond
01:03:25.924 MessageSleepForOneSecond
01:03:26.521 MessageSleepForOneSecond
01:03:26.710 MessageSleepForOneSecond
01:03:26.957 MessageSleepForOneSecond
01:03:27.107 MessageSleepForOneSecond
01:03:27.120 MessageSleepForOneSecond
01:03:27.126 MessageSleepForOneSecond
01:03:27.716 MessageSleepForOneSecond
01:03:27.906 MessageSleepForOneSecond
01:03:28.151 MessageSleepForOneSecond
01:03:28.305 MessageSleepForOneSecond
01:03:28.316 MessageSleepForOneSecond
01:03:28.322 MessageSleepForOneSecond
01:03:28.913 MessageSleepForOneSecond
01:03:29.100 MessageSleepForOneSecond
01:03:29.349 MessageSleepForOneSecond
01:03:29.502 MessageSleepForOneSecond
01:03:29.513 MessageSleepForOneSecond
01:03:29.538 MessageSleepForOneSecond
01:03:30.107 MessageSleepForOneSecond
01:03:30.297 MessageSleepForOneSecond
01:03:30.545 MessageSleepForOneSecond
01:03:30.705 MessageSleepForOneSecond
01:03:30.712 MessageSleepForOneSecond
01:03:30.733 MessageSleepForOneSecond
01:03:31.307 MessageSleepForOneSecond
01:03:31.310 MessageSleepForOneSecond
...

どうしたの?不正確さの原因は何ですか?ThreadPool の管理、Windows、...?

4

1 に答える 1

3

タイマーは無限に正確ではなく、スレッド スケジューリングも無限に正確ではありません。どちらの精度も最高で 1 ミリ秒 ( で設定可能timeBeginPeriod) で、現在のバージョンの Windows ではデフォルトで 16.7 ミリ秒 (15 年以上前の Windows では約 50 ミリ秒) です。

タイマーは、それ自体では無限に正確ではないため、スレッド プールからスレッドを準備状態にするイベントを生成します。このイベントは、後でスケジュールされますが、正確ではなく、厳密な保証もありません。他のスレッドが実行されている場合、「後で」は、プロセッサの負荷とスレッドの優先度に応じて、文字通りいつでも(または決して)遅くなる場合があります。

それだけで、選択した間隔が一般的に正確ではない (偶然を除いて) 十分な理由ですが、特に 250 ミリ秒の間隔は正確ではありません (249 ミリ秒はスケジューラのデフォルトの粒度の最も近い倍数であるため、新しいスレッドは265.6 ミリ秒後にスケジュールするのが最適です -- これは、コアがその正確な時間に利用可能であることを前提としていますが、これは保証されていません)。

また、HTTP リクエストの処理にはかなりの時間がかかる可能性があるため、他のスレッドがなくても、コアよりも多くのワーカーを作成する可能性が非常に高くなります。これは必然的に、特定の時間にどちらをスケジュールするかを OS が決定する必要があることを意味します。スケジューラが何をしようとも、最終的には常に 1 つまたは複数のスレッドに対して「不公平な」状況が発生し、それらが「不正確」になります。

コンソールへの書き込みについても同様です。コンソールへの書き込みは同期されます (つまり、2 つのスレッドがコンソールに書き込む場合、個々の書き込みは任意の順序で行われる可能性がありますが、文字化けすることはありません)。これは必然的にロックがあることを意味します。最初にロックを取得した人が続行します。1 ナノ秒後にロックを取得しようとする人は誰でもブロックされます。他のスレッドがタイム スライスを引き継ぎます。最終的に、ブロックされたスレッドは再び「準備完了」になりますが、このスレッドがすぐに実行されるという厳密な保証はありません。

走る準備ができていることと、走っていることはまったく別物です。

于 2013-03-10T13:34:00.940 に答える