Windows Via C/C+:ユーザーモードでのスレッド同期——原子操作:Interlocked関数ファミリー

6702 ワード

Windows Via C/C+:ユーザーモードでのスレッド同期——原子操作:Interlocked関数ファミリー
2009-10-30 21:12
2180人が読む
コメント(4)
コレクション
通報する
Windows alignment winapi threadコンパイラaccess
スレッド同期における原子操作の地位は非常に重要であり、スレッドがリソースにアクセスすると、他のスレッドが同じ時点でリソースにアクセスできないことを保証します.次のコードを例にとります.
// Define a global variable
long g_x = 0;

DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
  g_x ++;
  return 0;
}

DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
  g_x ++;
  return 0;
}

g_xはグローバル変数として宣言され、0に初期化されました.もし私が2つのスレッドを作成したら、1つはThreadFunc 1を実行し、もう1つはThreadFunc 2を実行します.ThreadFunc 1とThreadFunc 2の機能はすべてg_xに1を加える.ThreadFunc 1とThreadFunc 2が戻ってきたら、g_xの値は2であるべきだが、これはすべての可能な結果の1つにすぎず、g_を予測することはできない.xの最終値.コンパイラはg_になりますx++は以下のようなアセンブリコードを生成する:MOV EAX,[g_x];move the value in g_x into a register INC EAX ; increase the value in the register MOV [g_x], EAX ; store the new value back into g_x
2つのスレッドが同時に実行されると、MOV EAX,[g_x];thread 1: move 0 into a register INC EAX ; thread 1: increase the register to 1 MOV EAX, [g_x] ; thread 2: move 0 into the register again MOV [g_x], EAX ; thread 1: move 0 into g_x INC EAX ; thread 2: increase the register to 1 MOV [g_x], EAX ; thread 2: move 1 into g_x
上のコード実行後、g_xの最終値は1です!この結果は驚くべきことに,開発者はシステムスケジューラの行為にまったく関与できない.実際には、100個のスレッドがg_を同時に実行してもx++,g_xの最終値はまだ1かもしれません!これは我々の期待とは程遠いものであり,コンパイラによるコードの影響,CPUスケジューリングスレッドの挙動の影響,コンピュータシステムにインストールされたCPUの数の影響を受けることなく,0を2回に増やした結果がずっと2であることを望んでいる.幸いなことに、Windowsはいくつかの関数を提供して、私たちのコードが予想され、正しい結果を生むことを保証します.この問題を解決するために,自己増幅動作が原子的であることを保証しなければならない.すなわち,他のスレッドによって中断されない.Windowsが提供するinterlocked関数ファミリーは、このソリューションを提供します.Interlocked関数ファミリーはかなり簡単で理解しやすく,すべてのinterlocked関数のパラメータに対する動作は原子的である.たとえば、次のInterlockedExchangeAddおよびInterlockedExchangeAdd 64関数があります.
LONG InterlockedExchangeAdd(
  PLONG volatile plAddend,
  LONG lIncrement);
LONGLONG InterlockedExchangeAdd64(
  PLONGLONG volatile pllAddend,
  LONGLONG lIncrement);

上記の関数でg_を書き換えることができます.x++アクション:
long g_x = 0;

DWORD WINAPI ThreadFunc1(PVOID pvParam){
  InterlockedExchangeAdd(&g_x, 1);
  return 0;
}

DWORD WINAPI ThreadFunc2(PVOID pvParam){
  InterlockedExchangeAdd(&g_x, 1);
  return 0;
}

今、g_xの自己増加は原子であり,その最終値は2であることを保証する.注意値を1だけ増やす必要がある場合は、InterlockedIncrement関数を使用します.共有変数を変更しようとするすべてのスレッドは、簡単なC++算術演算の代わりにInterlocked関数ファミリーを呼び出す必要があります.
// the long variable shared by many threads
LONG g_x;

// Incorrect way to increase the variable
g_x ++;

// Correct way to increase the variable
InterlockedIncrement(&g_x);

Interlocked関数ファミリーの実装の詳細は、現在のCPUプラットフォームに依存します.x 86アーキテクチャのCPUでは、Interlocked関数は、ロックされたメモリ領域への他のスレッドのアクセスを阻止するために、バスにハードウェア信号を設定します.Interlocked関数ファミリーの実装の詳細は重要ではありません.重要なのは、コンパイラやCPUアーキテクチャ、数の影響を受けないパラメータの操作が原子であることを保証することです.Interlocked関数ファミリーに渡される変数アドレスは32ビット/64ビットで整列する必要があります.C実行時には_aligned_mallocとaligned_realloc関数は、指定したビット数で整列したメモリブロックを作成/再割り当てします:void*aligned_malloc(size_t size,size_t alignment)は、割り当てられるバイト数を表し、alignmentはメモリブロックを整列させるバイト境界であり、alignmentは2の倍数でなければならない.また、Interlocked関数ファミリーの実行速度はかなり速く、通常50 CPUサイクルを超えず、ユーザモードとカーネルモードとの間で変換する必要はない(この変換を実行するのに通常必要なCPUサイクル数は通常1000より大きい).
次に、指定した値を置き換えるinterlocked関数を3つ示します.
LONG InterlockedExchange(
  PLONG volatile plTarget,
  LONG lValue);

LONGLONG InterlockedExchange64(
  PLONGLONG volatile plTarget,
  LONGLONG lValue);

PVOID InterlockedExchangePointer(
  PVOID* volatile ppvTarget,
  PVOID pvValue);

関数InterlockedExchangeとInterlockedExchangePointerは、最初のパラメータアドレスの値を2番目のパラメータの値で置き換え、置換プロセスは原子操作です.32ビットアプリケーションでは、32ビットの値を32ビットの値で置き換えます.64ビットアプリケーションでは、InterlockedExchangeでは32値が使用されますが、InterlockedExchangePointerでは64ビットの値が使用されます.関数は、最初のパラメータアドレスの古い値を返します.InterlockedExchangeは、スピンロックの作成に役立ちます.
// Global variable indicating whether a shared resource is in user or not
BOOL g_fResourceInUser = FALSE;

void Func1() {
  // wait to access the resource
  while(InterlockedExchange(&g_fResourceInUse, TRUE) == TRUE)
    Sleep(0);
  // access the resource
  ...
  // reset the flag
  InterlockedExchange(&g_fResourceInUse, FALSE);
}

上のコードの意味ははっきりしていて、あまり説明しません.スピンロックはCPU時間の無駄になるので注意してください.スピンロックでは、CPUは、ループから飛び出すかどうかを決定する2つの値を常に比較しなければならない.上記のコードでは、スピンロックを使用するすべてのスレッドの優先度が同じでなければなりません.そうしないと、スピンロック内にあるスレッドが優先度が高い場合、CPUはスピンロックのループに陥り続けます.また、スピンロックを使用するスレッドに対してSetProcessPriorityBoost/SetThreadPriorityBoostを呼び出す必要があります.
スピンロックを使用する場合、スレッドが保護されたリソースにアクセスする時間はできるだけ短くする必要があります.より効果的な方法は、まずスピンロック待ちを使用し、一定時間が経過しても保護されたリソースにアクセスできない場合、リソースが他のスレッドによって解放されるまでカーネルモード待ちに移行することであり、これが臨界領域の実現原理である.
次に、最後の2つの交換Interlocked値置換関数を示します.
LONG InterlockedCompareExchange(
  PLONG plDestination,
  LONG lExchange,
  LONG lCompared);

PVOID InterlockedCompareExchangePointer(
  PVOID* ppvDestination,
  PVOID pvExchange,
  PVOID pvCompared);

関数は現在の値(plDestination/ppvDestination)とlCompared/pvComparedを比較し、両者が等しい場合、現在の値はlExchange/pvExchangeに置き換えられます.そうしないと、現在の値は変わらず、関数は現在の値の古い値を返します.
Windowsでは、上記のほかにInterlocked関数もいくつか用意されていますが、これらの関数は、次のような上の関数で実現されています.
LONG InterlockedIncrement(PLONG plAddend);

LONG InterlockedDecrement(PLONG plAddend);

この2つの関数は明らかにInterlockedExchangeAddによって実現できる.このほか、InterlockedCompareExchange 64に基づいて実装されるOR、XOR、AND操作用のInterlocked関数(InterlockedAnd 64など)もあります.
LONGLONG InterlockedAnd64(LONGLONG* Destination, LONGLONG value) {
  LONGLONG old = *Destination;
  do {
    old = *Destination;
  } while(InterlockedCompareExchange64(Destination, old&value, old) != old);
  return old;
}

                 :
LONGLONG InterlockedAnd64(LONGLONG* Destination, LONGLONG value) {
  return InterlockedCompareExchange64(Destination, (*Destination)&value, *Destination);
}

Windows XPから、整数とブール値の原子操作に加えて、開発者は新しい関数を使用して「Interlocked Singly Linked List」と呼ばれるスタックを操作することができます.スタック上の各アクション(たとえば、スタックの圧縮、イジェクトなど)は原子操作であり、次の表にこれらの関数を示します.
関数名
説明
InitializeSListHead
Creates an empty stack
InterlockedPushEntrySList
Adds an element on top of the stack
InterlockedPopEntrySList
Removes the top element of the stack and returns it
InterlockedFlushSList
Empties the stack
QueryDepthSList
Returns the number of elements stored in the stack