2-4 非ブロッキング・ロックの使用

概要

スレッドは、サポートされているスレッドモデルの実装で提供される同期プリミティブを実行して、共有リソースで同期します。同期プリミティブ (mutex やセマフォーなど) は、1 つのスレッドだけがロックを保持できるようにし、その間ほかのスレッドはタイムアウト・メカニズムに応じてスピンまたはブロックされるようにします。ブロッキングはコストの高いコンテキスト・スイッチを発生させ、スピンは CPU 実行リソースを (短時間だけ使用される場合を除いて) 浪費します。一方、非ブロッキング・システムコールは、競合スレッドがロックに失敗した場合、リターンして有益な処理を行えるようにし、実行リソースの浪費を回避します。

この記事は、「マルチスレッド・アプリケーションの開発のためのインテル・ガイド」の一部で、インテル® プラットフォーム向けにマルチスレッド・アプリケーションを効率的に開発するための手法について説明します。

はじめに

Windows* スレッド API や POSIX* スレッド API を含むほとんどのスレッド化の実装では、ブロッキングと非ブロッキングの両方のスレッド同期プリミティブが提供されています。通常、デフォルトでは、ブロッキング・プリミティブが使用されます。ブロッキング・プリミティブでは、ロックに成功すると、スレッドはロックの制御を取得し、クリティカル・セクションでコードを実行します。ただし、ロックに失敗した場合は、コンテキスト・スイッチが発生し、スレッドは待機中のスレッドのキューに入れられます。コンテキスト・スイッチには、次のようなコストがかかるため、できるだけ避ける必要があります。

  • コンテキスト・スイッチには大きなオーバーヘッドが伴い、特にカーネルスレッドを使用したスレッドの実装ではこれが顕著です。
  • アプリケーションで同期コールの後にある処理は、スレッドがロックを取得するまで実行を待機しなければなりません。

非ブロッキング・システムコールを使用することで、これらのパフォーマンス・ペナルティーを軽減できます。非ブロッキング・システムコールでは、クリティカル・セクションのロックに失敗しても、アプリケーション・スレッドは実行を再開します。そのため、コンテキスト・スイッチに伴うオーバーヘッドとロックでの不必要なスピンが排除され、スレッドは再度ロックの取得を試みるまで、有益な処理を行うことができます。

アドバイス

コンテキスト・スイッチに伴うオーバーヘッドを回避するために、できるだけ非ブロッキングの呼び出しを使用してください。通常、非ブロッキングの同期コールは try で始まります。例えば、Windows スレッド API では、クリティカル・セクションの同期プリミティブとして、以下のようにブロッキングと非ブロッキングの両方が提供されています。

クリティカル・セクションのロックの取得に成功すると、TryEnterCriticalSection は、Boolean 値 True を返します。失敗した場合は False が返され、スレッドはアプリケーション・コードの実行を再開します。

void EnterCriticalSection (LPCRITICAL_SECTION cs);
bool TryEnterCriticalSection (LPCRITICAL_SECTION cs);

次に、非ブロッキング・システムコールの一般的な使用例を示します。

CRITICAL_SECTION cs;
void threadfoo()
{
  while(TryEnterCriticalSection(&cs) == FALSE)
  {
  // some useful work
  }
    // Critical Section of Code
    LeaveCriticalSection (&cs);
  }
  // other work
}

同様に、POSIX スレッドでも mutex、セマフォー条件変数の非ブロッキング同期プリミティブが提供されています。例えば、mutex 同期プリミティブには、以下のようにブロッキングと非ブロッキングの両方があります。

int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_try_lock (pthread_mutex_t *mutex);

Windows スレッドの実装では、ロッキング・プリミティブでタイムアウトを指定することもできます。Win32* API では、カーネル・オブジェクトで同期をとるための WaitForSingleObject システムコールと WaitForMultipleObjects システムコールが提供されています。これらのシステムコールを実行するスレッドは、該当するカーネル・オブジェクトがシグナル状態になるか、ユーザーによって指定された待機時間が経過するまで待機します。待機時間が経過すると、スレッドは処理の実行を再開します。

DWORD WaitForSingleObject (HANDLE hHandle, DWORD dwMilliseconds);

このコードでは、hHandle はカーネル・オブジェクトのハンドル、dwMilliseconds は待機時間です。待機時間が経過しても、カーネル・オブジェクトがシグナル状態にならなかった場合、この関数は戻ります。待機時間の値を INFINITE に設定すると、スレッドは無制限に待機します。次のサンプルコードは、この API コールの使用例を示したものです。

void threadfoo ()
{
  DWORD ret_value;
  HANDLE hHandle;
    // Some work
  ret_value = WaitForSingleObject (hHandle,0);
  if (ret_value == WAIT_TIME_OUT)
{ 
    // Thread could not gain ownership of the kernel 
    // object within the time interval;
// Some useful work
    }
    else if (ret_value == WAIT_OBJECT_0)
{
    // Critical Section of Code
    }
    else { // Handle Wait Failure}
    // Some work
}

また同様に、WaitForMultipleObjects API コールでも、複数のカーネル・オブジェクトのシグナル状態を待機することができます。

非ブロッキングの同期コール (例えば TryEnterCriticalSection など) を使用する場合は、共有オブジェクトを解放する前に、戻り値をチェックして要求が成功したかどうかを確認する必要があります。