4

スレッドを同期するために2つの機能を必要とする高性能アプリを構築しています

void wake_thread(thread)

void sleep_thread(thread)

アプリには、sleep_threadを呼び出すとスリープ状態になる可能性のある単一のスレッド(Cと呼びます)があります。wake_threadを呼び出す複数のスレッドがあります。wake_threadが戻るとき、Cが実行されているか、ウェイクアップされることを保証する必要があります。wake_threadは決してブロックしてはなりません。

もちろん、簡単な方法は次のような同期イベントを使用することです。

hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);

void wake_thread(thread) {

  SetEvent(hEvent);
}

と:

void sleep_thread(thread)
{
  WaitForSingleObject(hEvent);
}

これにより、必要なセマンティクスが提供され、シナリオの競合状態が発生しなくなります(待機しているスレッドは1つだけですが、シグナルを送信できるスレッドは複数あります)。何を調整しようとしているのかを示すために、ここに含めました。

ただし、この非常に特殊なシナリオでは、Windowsでより高速な方法があるのではないかと思います。Cがスリープしていない場合でも、wake_threadが頻繁に呼び出されることがあります。これにより、何もしないSetEventへの多くの呼び出しが発生します。手動リセットイベントと参照カウンターを使用して、実際に設定するものがある場合にのみSetEventが呼び出されるようにするためのより高速な方法はありますか。

このシナリオでは、すべてのCPUサイクルが重要です。

4

3 に答える 3

2

SetEvent()sysenterオブジェクトマネージャがイベントの状態をチェックして(への呼び出しを介して)ディスパッチするためにシステムコール(ユーザーモードからカーネルモードへの切り替えをトリガーする)を行う必要があるため、ある程度の遅延が発生しますKeSetEvent()。システムコールの時間は、あなたの状況でも許容できると思われるかもしれませんが、それは推測です。レイテンシーの大部分が発生する可能性が高いのは、イベントの受信側です。WaitFor*Object()つまり、イベントを通知するよりも、スレッドをからウェイクアップするのに時間がかかります。Windowsスケジューラは、待機リターンがあるスレッドに優先度の「ブースト」を与えることで、スレッドに早く到達できるように支援しようとしますが、そのブーストはそれほど多くのことを行いません。

これを回避するには、必要な場合にのみ待機していることを確認する必要があります。これを行うための一般的な方法は、消費者の場合、行くように合図されたら、イベントを再度待たずにできるすべての作業項目を消費し、完了したらに電話をかけることです。sleep_thread()

SetEvent()/WaitFor*Object()は、100%CPUを消費する以外のすべてよりもほぼ確実に高速であり、共有データを保護するために必要なロックオブジェクトの競合の結果として、それでも高速になる可能性があることを指摘しておく必要があります。

通常、ConditionVariableの使用をお勧めしますが、あなたのテクニックと比較してそのパフォーマンスをテストしていません。CRITICAL_SECTIONオブジェクトに入るオーバーヘッドもあるので、遅くなるのではないかと疑っています。パフォーマンスを異なる方法で測定する必要がある場合があります。疑わしい場合は、測定、測定、測定します。

私が言える他の唯一のことは、MSは、特に繰り返し実行される場合、イベントのディスパッチと待機が遅くなる可能性があることを認識しているということです。これを回避するために、彼らはCRITICAL_SECTION実際にイベントを待つ前に、ユーザーモードで何度もロックを取得しようとするようにオブジェクトを変更しました。彼らはこれをスピンカウントと呼んでいます。私はそれをお勧めしませんが、あなたは同じようなことをすることができるかもしれません。

于 2013-01-02T20:52:34.377 に答える
2

私はこれをテストしていません(コンパイルを確認することは別として)が、これでうまくいくと思います。確かに、私が最初に思ったよりも少しトリッキーでした。いくつかの明らかな最適化を行うことができることに注意してください。わかりやすくするため、また必要になる可能性のあるデバッグを支援するために、最適化されていない形式のままにしておきます。エラーチェックも省略しました。

#include <intrin.h>

HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
__declspec(align(4)) volatile LONG thread_state = 2;
    // 0 (00): sleeping
    // 1 (01): sleeping, wake request pending
    // 2 (10): awake, no additional wake request received
    // 3 (11): awake, at least one additional wake request

void wake_thread(void)
{
    LONG old_state;

    old_state = _InterlockedOr(&thread_state, 1);
    if (old_state == 0)
    {
        // This is the first wake request since the consumer thread
        // went to sleep.  Set the event.

        SetEvent(hEvent);
        return;
    }
    if (old_state == 1)
    {
        // The consumer thread is already in the process of being woken up.
        // Any items added to the queue by this thread will be processed,
        // so we don't need to do anything.

        return;
    }
    if (old_state == 2)
    {
        // This is an additional wake request when the consumer thread
        // is already awake.  We've already changed the state accordingly,
        // so we don't need to do anything else.

        return;
    }
    if (old_state == 3)
    {
        // The consumer thread is already awake, and already has an
        // additional wake request registered, so we don't need to do
        // anything.

        return;
    }
    BigTrouble();
}

void sleep_thread(void)
{
    LONG old_state;

    // Debugging only, remove this test in production code.
    // The event should never be signaled at this point.

    if (WaitForSingleObject(hEvent, 0) != WAIT_TIMEOUT)
    {
        BigTrouble();
    }

    old_state = _InterlockedAnd(&thread_state, 1);
    if (old_state == 2)
    {
        // We've changed the state from "awake" to "asleep".
        // Go to sleep.

        WaitForSingleObject(hEvent, INFINITE);

        // We've been buzzed; change the state to "awake"
        // and then reset the event.

        if (_InterlockedExchange(&thread_state, 2) != 1)
        {
            BigTrouble();
        }
        ResetEvent(hEvent);
        return;
    }
    if (old_state == 3)
    {
        // We've changed the state from "awake with additional
        // wake request" to "waking".  Change it to "awake"
        // and then carry on.

        if (_InterlockedExchange(&thread_state, 2) != 1)
        {
            BigTrouble();
        }
        return;
    }
    BigTrouble();
}

基本的に、これは手動リセットイベントと2ビットフラグを使用して、自動リセットイベントの動作を再現します。状態図を描くと、より明確になる場合があります。スレッドセーフは、どの関数がどの遷移を実行できるかに関するルールと、イベントオブジェクトのシグナリングがいつ許可されるかによって異なります。

社説として:同期コードをwake_thread関数とsleep_thread関数に分離しているため、少し厄介だと思います。同期コードがキューの実装に移動された場合は、おそらくより自然で、わずかに効率的で、ほぼ確実に明確になります。

于 2013-01-03T02:24:54.517 に答える
0

何かのようなもの:

void consumer_thread(void)
{
   while(1)
   {
      WaitForSingleObject(...);

      // Consume all items from queue in a thread safe manner (e.g. critical section)
   }
}

void produce()
{
   bool queue_was_empty = ...; // in a thread safe manner determine if queue is empty
   // thread safe insertion into queue ...
   // These two steps should be done in a way that prevents the consumer 
   // from emptying the queue in between, e.g. a spin lock.  
   // This guarantees you will never miss the "edge"
   if( queue_was_empty )
   {
      SetEvent(...);
   }
}

一般的な考え方は、空から満杯への移行時にのみSetEventを実行することです。スレッドの優先度が同じである場合、Windowsはプロデューサーを実行し続ける必要があるため、キューの挿入ごとのSetEvent呼び出しの数を最小限に抑えることができます。この配置(同じ優先度のスレッド間)で最高のパフォーマンスが得られることがわかりました(少なくともWindows XPとWin7、YMMVでは)。

于 2013-01-03T02:41:51.800 に答える