マルチスレッド開発ガイド: 2.4 非ブロッキング・ロックの使用

インテル® Parallel Studio XE特集

この記事は、インテル® ソフトウェア・ネットワークに掲載されている「Use Non-blocking Locks When Possible」 (https://software.intel.com/content/www/us/en/develop/articles/use-non-blocking-locks-when-possible.html) の日本語参考訳です。


編集注記:
本記事は、2011 年 5 月 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)
  {
  // ワーク
  }
    // Critical Section of Code
    LeaveCriticalSection (&cs);
  }
  // その他のワーク
}

同様に、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;
    // ワーク
  ret_value = WaitForSingleObject (hHandle,0);

  if (ret_value == WAIT_TIME_OUT)
{
    // スレッドはインターバル期間内にカーネル・
    // オブジェクトの所有権を取得できませんでした
// ワーク
    }
    else if (ret_value == WAIT_OBJECT_0)
{
    // クリティカル・セクションのコード
    }
    else { // 待機失敗を処理 }
    // ワーク
}

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

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

タイトルとURLをコピーしました