Windows ではどのような同期 API が利用できますか? スレッドの状態。 変数への保護されたアクセス

19.10.2023

複数のスレッドまたはプロセスを使用する場合、場合によっては、 実行を同期するそのうちの 2 つ以上。 この理由は、ほとんどの場合、2 つ以上のスレッドが共有リソースへのアクセスを必要とする可能性があるためです。 本当に複数のスレッドに同時に提供することはできません。 共有リソースは、実行中の複数のタスクから同時にアクセスできるリソースです。

同期プロセスを確実に実行するメカニズムが呼び出されます。 アクセス制限。これは、あるスレッドが別のスレッドによって生成されたイベントを待機している場合にも必要になります。 当然のことながら、イベントが発生する前に最初のスレッドを一時停止する何らかの方法が必要です。 この後、スレッドは実行を継続する必要があります。

タスクが取り得る一般的な状態は 2 つあります。 まず、タスクでできることは、 実行される(または、CPU リソースにアクセスできるようになるとすぐに実行できるようになります)。 第二に、タスクは次のとおりです。 ブロックされました。この場合、必要なリソースが解放されるか、特定のイベントが発生するまで、実行は一時停止されます。

Windows には、共有リソースへのアクセスを特定の方法で制限できる特別なサービスがあります。これは、オペレーティング システムの助けがなければ、別のプロセスまたはスレッドがリソースに単独でアクセスできるかどうかを自分で判断できないためです。 Windows オペレーティング システムには、1 回の連続操作中にリソース アクセス フラグをチェックし、可能であれば設定するプロシージャが含まれています。 オペレーティング システム開発者の言語では、この操作は次のように呼ばれます。 動作確認と取り付け。 同期を提供し、リソースへのアクセスを制御するために使用されるフラグは、と呼ばれます。 セマフォ(セマフォ)。 Win32 API は、セマフォおよびその他の同期オブジェクトのサポートを提供します。 MFC ライブラリには、これらのオブジェクトのサポートも含まれています。

同期オブジェクトと MFC クラス

Win32 インターフェイスは 4 種類の同期オブジェクトをサポートしています。それらはすべて何らかの形でセマフォの概念に基づいています。

最初のタイプのオブジェクトはセマフォ自体です。または、 クラシック (標準) セマフォ。 これにより、限られた数のプロセスとスレッドが単一のリソースにアクセスできるようになります。 この場合、リソースへのアクセスは完全に制限されるか (一定期間内に 1 つのスレッドまたはプロセスのみがリソースにアクセスできる)、または少数のスレッドおよびプロセスのみが同時にアクセスを受けます。 セマフォは、セマフォがタスクに割り当てられると値が減少し、タスクがセマフォを解放すると値が増加するカウンタを使用して実装されます。

2 番目のタイプの同期オブジェクトは次のとおりです。 排他的 (ミューテックス) セマフォ。 これは、リソースへのアクセスを完全に制限し、常に 1 つのプロセスまたはスレッドのみがリソースにアクセスできるように設計されています。 実際、これは特殊なタイプのセマフォです。

3 番目のタイプの同期オブジェクトは次のとおりです。 イベント、 または イベントオブジェクト。これは、他のプロセスまたはスレッドがリソースを使用できると宣言するまで、リソースへのアクセスをブロックするために使用されます。 したがって、このオブジェクトは必要なイベントの完了を通知します。

4 番目のタイプの同期オブジェクトを使用すると、複数のスレッドによるプログラム コードの特定のセクションの同時実行を禁止できます。 これを行うには、これらの領域を次のように宣言する必要があります。 クリティカルセクション。 1 つのスレッドがこのセクションに入ると、最初のスレッドがセクションを出るまで、他のスレッドが同じことを行うことは禁止されます。

クリティカル セクションは、他のタイプの同期オブジェクトとは異なり、同じプロセス内のスレッドを同期するためにのみ使用されます。 他のタイプのオブジェクトを使用して、プロセス内のスレッドを同期したり、プロセスを同期したりできます。

MFC では、Win32 インターフェイスによって提供される同期メカニズムは、CSyncObject クラスから派生した次のクラスによってサポートされます。

    Cクリティカルセクション- クリティカルセクションを実装します。

    Cイベント- イベントオブジェクトを実装します

    Cミューテックス- 排他的セマフォを実装します。

    CSセマフォ- 古典的なセマフォを実装します。

これらのクラスに加えて、MFC は 2 つの補助同期クラスも定義します。 Cシングルロックそして Cマルチロック。 これらは、同期オブジェクトへのアクセスを制御し、そのようなオブジェクトを提供および解放するために使用されるメソッドを含みます。 クラス Cシングルロック単一の同期オブジェクトとクラスへのアクセスを制御します。 Cマルチロック- 複数のオブジェクトに。 以下ではクラスのみを考慮します Cシングルロック.

同期オブジェクトが作成されると、クラスを使用してそのオブジェクトへのアクセスを制御できます。 Cシングルロック。 これを行うには、まず次のタイプのオブジェクトを作成する必要があります。 Cシングルロックコンストラクターを使用して:

Cシングルロック(CSyncObject* pObject, BOOL bInitialLock = FALSE);

最初のパラメータは、セマフォなどの同期オブジェクトへのポインタを渡します。 2 番目のパラメーターの値は、コンストラクターがこのオブジェクトへのアクセスを試行するかどうかを決定します。 このパラメータがゼロ以外の場合、アクセスが取得されます。それ以外の場合、アクセスは試行されません。 アクセスが許可されると、クラス オブジェクトを作成したスレッドが Cシングルロック、対応する同期オブジェクトがメソッドによって解放されるまで停止されます。 ロックを解除するクラス Cシングルロック.

CSingleLock タイプのオブジェクトが作成されると、pObject が指すオブジェクトへのアクセスは、次の 2 つの関数を使用して制御できます。 ロックそして ロックを解除するクラス Cシングルロック.

方法 ロック同期オブジェクトへのアクセスを取得することを目的としています。 それを呼び出したスレッドは完了するまで一時停止されます この方法つまり、リソースがアクセスされるまでです。 パラメーターの値によって、関数が必要なオブジェクトへのアクセスを待機する時間が決まります。 メソッドが正常に完了するたびに、同期オブジェクトに関連付けられたカウンターの値が 1 ずつ減ります。

方法 ロックを解除する同期オブジェクトを解放し、他のスレッドがリソースを使用できるようにします。 このメソッドの最初のバージョンでは、このオブジェクトに関連付けられたカウンターの値が 1 ずつ増加します。 2 番目のオプションでは、最初のパラメータによって、この値をどれだけ増やす必要があるかが決まります。 2 番目のパラメーターは、前のカウンター値が書き込まれる変数を指します。

クラスで作業するとき Cシングルロックリソースへのアクセスを制御する一般的な手順は次のとおりです。

    リソースへのアクセスを制御するために使用されるタイプ CSyncObj のオブジェクト (セマフォなど) を作成します。

    ソズの助けを借りて このオブジェクトの同期は CSingleLock 型のオブジェクトを作成します。

    リソースにアクセスするには、Lock メソッドを呼び出します。

    リソースにアクセスします。

    Unlock メソッドを呼び出してリソースを解放します。

以下に、セマフォとイベント オブジェクトの作成方法と使用方法について説明します。 これらの概念を理解すると、他の 2 つのタイプの同期オブジェクト (クリティカル セクションとミューテックス) を簡単に学習して使用できるようになります。

スレッドの同期

マルチスレッド アプリケーションを構築するときは、共有データのあらゆる部分が、複数のスレッドによって値が変更される可能性から保護されていることを確認する必要があります。 AppDomain 内のすべてのスレッドが共有アプリケーション データに同時にアクセスできることを考慮して、複数のスレッドが同じデータ項目に同時にアクセスした場合に何が起こるかを想像してください。 スレッド スケジューラはスレッドをランダムに一時停止するため、スレッド A が実行を終了する前に中断された場合はどうなるでしょうか? 次に、スレッド B が不安定なデータを読み取ります。

同時実行性の問題を説明するために、次の例を考えてみましょう。

Public class MyTheard ( public void ThreadNumbers() ( // スレッドに関する情報 Console.WriteLine("(0) スレッドは ThreadNumbers メソッドを使用します",Thread.CurrentThread.Name); // 数値を出力します Console.Write("Numbers : "); for (int i = 0; i

テストの実行を確認する前に、問題をもう一度明確にしましょう。 このアプリケーション ドメイン内のプライマリ スレッドは、10 個のセカンダリ ワーカー スレッドを生成することでその動作を開始します。 各ワーカー スレッドは、同じ MyTheard インスタンスで ThreadNumbers() メソッドを呼び出す必要があります。 このオブジェクト (コンソール) の共有リソースをロックするための手段が講じられていないとすると、ThreadNumbers() メソッドが完全な結果を出力する前に現在のスレッドが強制終了される可能性が高くなります。 これがいつ起こるかは正確には分からないため (または、そもそも起こるかどうかさえ)、予期しない結果が発生する可能性があります。 たとえば、次の出力が表示される場合があります。

明らかにここに問題があります。 各スレッドが MyTheard に数値データの出力を要求すると、スレッド スケジューラは数値データを交換します。 背景。 その結果、一貫性のない出力が生成されます。 C# でこのような問題を解決するために使用するのは、 同期.

同期はロックの概念に基づいており、オブジェクト内のコード ブロックへのアクセス制御が組織化されます。 オブジェクトが 1 つのスレッドによってロックされている場合、他のスレッドはロックされたコード ブロックにアクセスできません。 あるスレッドによってロックが解放されると、そのオブジェクトは別のスレッドで使用できるようになります。

ロック機能は C# 言語に組み込まれています。 これにより、すべてのオブジェクトを同期することができます。 同期はキーワードを使用して構成されます ロック。 C# には最初から組み込まれているため、一見したよりもはるかに使いやすいです。 実際、オブジェクトの同期は、多くの C# プログラムでほぼシームレスに行われます。

以下はブロックの一般的な形式です。

lock(lockObj) ( // 同期されたステートメント)

どこ ロックオブジェクトは同期されたオブジェクトへの参照を示します。 1 つのステートメントのみを同期する必要がある場合は、中括弧は必要ありません。 ロック演算子は、特定のオブジェクトのロックによって保護されたコードが、そのロックを取得したスレッドによってのみ使用されることを保証します。 そして、ロックが解放されるまで、他のすべてのスレッドはブロックされます。 ロックは、保護されているコードフラグメントが完了すると解放されます。

同期されたリソースを表すオブジェクトはロックされているとみなされます。 場合によっては、それがリソース自体のインスタンスであるか、同期に使用されるオブジェクトの任意のインスタンスであることが判明します。 ただし、ロックされたオブジェクトはパブリックにアクセスできないことに注意してください。そうしないと、プログラム内の別の制御されていないコード部分からロックされ、その後まったくロックが解除されなくなる可能性があります。

以前は、オブジェクトをロックするために lock (this) 構造が非常に一般的に使用されていました。 ただし、これが役立つのは、プライベート オブジェクトへの参照である場合のみです。 ロック (この) 構造によってプログラミングおよび概念的なエラーが発生する可能性があるため、その使用は推奨されなくなりました。 代わりに、プライベート オブジェクトを作成してロックすることをお勧めします。

前の例に同期を追加して変更してみましょう。

Public class MyTheard ( private object threadLock = new object(); public void ThreadNumbers() ( // ロック マーカー ロック (threadLock) を使用します ( // スレッドに関する情報 Console.WriteLine("(0) スレッドは ThreadNumbers メソッドを使用します" , Thread.Name); // 数値を出力します Console.Write("Numbers: ");

スレッドがロック コンテキストに入ると、ロック コンテキストを終了してロックが解放されるまで、他のスレッドはロック トークン (この場合は現在のオブジェクト) を利用できなくなります。 したがって、スレッド A がロック トークンを取得すると、スレッド A がロック トークンを解放するまで、他のスレッドは同じトークンを使用してコンテキストに入ることができなくなります。

複数のプロセス (または同じプロセスの複数のスレッド) が同時にリソースにアクセスすると、同期の問題が発生します。 Win32 のスレッドは、事前に認識されていない時点でいつでも停止できるため、スレッドの 1 つがリソース (ファイルにマップされたメモリ領域など) の変更を完了する時間がなかった場合、状況が発生する可能性があります。 ) が停止され、別のスレッドが同じリソースにアクセスしようとしました。 現時点では、リソースは一貫性のない状態にあり、それにアクセスすると、データの破損からメモリ保護の違反まで、最も予期せぬ結果が生じる可能性があります。

Win32 でのスレッド同期の背後にある主な考え方は、同期オブジェクトと待機関数の使用です。 オブジェクトは、シグナルありまたはシグナルなしの 2 つの状態のいずれかになります。 待機関数は、指定されたオブジェクトが Not Signaled 状態である限り、スレッドの実行をブロックします。 したがって、リソースへの排他的アクセスが必要なスレッドは、何らかの同期オブジェクトを非シグナル状態に設定し、完了時にそれをシグナル状態にリセットする必要があります。 他のスレッドは、このリソースにアクセスする前に待機関数を呼び出す必要があります。これにより、他のスレッドはリソースが使用可能になるまで待機できるようになります。

Win32 API が提供するオブジェクトと同期関数を見てみましょう。

同期機能

同期関数は、単一のオブジェクトを待機する関数と、複数のオブジェクトの 1 つを待機する関数の 2 つの主なカテゴリに分類されます。

単一のオブジェクトを想定する関数

最も単純な待機関数は WaitForSingleObject 関数です。

Function WaitForSingleObject(hHandle: THandle; // オブジェクト識別子 dwMilliseconds: DWORD // 待機期間): DWORD; 標準呼び出し;

この関数は、hHandle オブジェクトがシグナル状態になるまで dwMilliseconds ミリ秒待機します。 dwMilliseconds パラメータとして INFINITE を渡すと、関数は無期限に待機します。 dwMilliseconds が 0 の場合、関数はオブジェクトの状態をチェックし、すぐに戻ります。

この関数は、次のいずれかの値を返します。

次のコード スニペットは、ObjectHandle オブジェクトがシグナル状態になるまで、Action1 へのアクセスを拒否します (たとえば、この方法では、CreateProcess 関数で取得した識別子として ObjectHandle を渡すことで、プロセスが完了するのを待つことができます)。

Var 理由: DWORD;<>エラーコード: DWORD;

アクション 1.有効:= FALSE;

Application.ProcessMessages を繰り返してみてください。 理由:= WailForSingleObject(ObjectHandle, 10); Reason = WAIT_FAILED の場合、ErrorCode:= GetLastError を開始します。

raise Exception.CreateFmt('オブジェクトの待機がエラーで失敗しました: %d', );

終わり; 理由まで

WAIT_TIMEOUT;

最後に Actionl.Enabled:= TRUE;

終わり;

オブジェクトを待機すると同時に、別のオブジェクトをシグナル状態にする必要がある場合は、SignalObjectAndWait 関数を使用できます。

Function SignalObjectAndWait(hObjectToSignal: THandle; // 信号状態に転送されるオブジェクト // hObjectToWaitOn: THandle; // オブジェクト,

どれの

関数は dwミリ秒を想定しています: DWORD; // 待機期間 bAlertable: BOOL // I/O 操作を完了する要求が発生した場合に、関数が // 制御を返すかどうかを指定します): DWORD; 標準呼び出し;

Function SignalObjectAndWait(hObjectToSignal: THandle; // 信号状態に転送されるオブジェクト // hObjectToWaitOn: THandle; // オブジェクト,

戻り値はWaitForSingleObject関数と同じです。

hObjectToSignal オブジェクトは、セマフォ、イベント、またはミューテックスにすることができます。 bAlertable パラメーターは、オペレーティング システムがスレッドに非同期 I/O 操作または非同期プロシージャ コールの完了を要求した場合に、オブジェクトの待機を中断するかどうかを決定します。 これについては、以下でさらに詳しく説明します。 WAIT_ABANDONED_0、スレッドは lpHandles 配列内のオブジェクトのインデックスを受け取ります。 この物件の所有者合図せずに終わった
WAIT_TIMEOUT 待機期間が終了しました
WAIT_FAILED エラーが発生しました

たとえば、次のコード部分では、プログラムはスレッド間で共有される 2 つの異なるリソースを変更しようとします。

Var Handles: THandle の配列。

理由: DWORD。 RestIndex: 整数;... ハンドル := OpenMutex(SYNCHRONIZE, FALSE, 'FirstResource');

ハンドル := OpenMutex(SYNCHRONIZE, FALSE, 'SecondResource');

この関数と前の関数の主な違いは dwWakeMask パラメータです。これは QS_XXX ビット フラグの組み合わせであり、待機しているオブジェクトの状態に関係なく、関数の待機を中断するメッセージのタイプを指定します。 たとえば、QS_KEY マスクを使用すると、WM_KEYUP、WM_KEYDOWN、WM_SYSKEYUP、または WM_SYSKEYDOWN メッセージがキューに表示されたときに待機を中断できます。また、QS_PAINT マスクを使用すると、WM_PAINT メッセージが許可されます。 dwWakeMask に許可される値の完全なリストについては、Windows SDK のドキュメントを参照してください。 指定されたマスクに対応するメッセージが関数を呼び出したスレッドのキューに出現すると、関数は値 WAIT_OBJECT_0 + nCount を返します。 プログラムがこの値を受け取ると、それを処理し、再度 wait 関数を呼び出すことができます。 外部アプリケーションを起動する例を考えてみましょう (呼び出し側プログラムは実行中にユーザー入力に応答しないことが必要ですが、そのウィンドウは再描画され続ける必要があります)。

プロシージャ TForm1.Button1Click(送信者: TObject);

待機関数を呼び出すスレッドで Windows ウィンドウが明示的 (CreateWindow 関数を使用) または暗黙的 (TForm、DDE、COM を使用) に作成された場合、スレッドはメッセージを処理する必要があります。 ブロードキャスト メッセージはシステム内のすべてのウィンドウに送信されるため、メッセージを処理しないスレッドはデッドロックを引き起こす可能性があります (システムは、スレッドがメッセージを処理するか、システムのスレッドまたは他のスレッドがオブジェクトを解放するのを待ちます)。 Windows がハングアップする原因となります。 プログラムにそのようなフラグメントがある場合は、MsgWaitForMultipleObjects または MsgWaitForMultipleObjectsEx を使用し、メッセージの処理の待機を中断できるようにする必要があります。 アルゴリズムは上記の例と似ています。

I/O または APC 操作を完了するための要求を待機する割り込み

Windows は非同期プロシージャ コールをサポートしています。 各スレッドが作成されると、非同期プロシージャ呼び出しのキュー (APC キュー) が関連付けられます。 オペレーティング システム (または QueueUserAPC 関数を使用するユーザー アプリケーション) は、特定のスレッドのコンテキストで関数を実行するリクエストを発行できます。 スレッドがビジー状態である可能性があるため、これらの関数はすぐには実行できません。 したがって、スレッドが次のいずれかの待機関数を呼び出すと、オペレーティング システムはこれらの関数を呼び出します。

Function SleepEx(dwMilliseconds: DWORD; // 待機期間 bAlertable: BOOL // 非同期プロシージャ呼び出しの要求が発生した場合に、 // 関数が制御を返すかどうかを指定します): DWORD; 標準呼び出し;

function WaitForSingleObjectEx(hHandle: THandle; // オブジェクト識別子 dwMilliseconds: DWORD; // 待機期間 bAlertable: BOOL // 非同期プロシージャ呼び出しの要求が発生した場合に、関数が // 制御を返すかどうかを設定します): DWORD; 標準呼び出し;

このメカニズムにより、たとえば非同期 I/O の実装が可能になります。 スレッドは、ReadFileEx 関数または WriteFileEx 関数に操作の完了ハンドラー関数のアドレスを渡すことで、1 つ以上の I/O 操作のバックグラウンド実行を開始できます。 完了すると、これらの関数への呼び出しは非同期プロシージャ呼び出しのキューに入れられます。 次に、操作を開始したスレッドは、結果を処理する準備ができたら、上記の待機関数のいずれかを使用して、オペレーティング システムがハンドラー関数を呼び出すことを許可できます。 APC キューは OS カーネル レベルで実装されるため、メッセージ キューよりも効率が高く、より効率的な I/O が可能になります。

イベント

イベントを使用すると、イベントが発生したことを待機中の 1 つ以上のスレッドに通知できます。 イベントが発生します:

オブジェクトを作成するには、CreateEvent 関数を使用します。

Function CreateEvent(lpEventAttributes: PSecurityAttributes; // 構造体のアドレス // TSecurityAttributes bManualReset, // イベントを切り替えるかを // 手動 (TRUE) または自動 (FALSE) で設定します。 bInitialState: BOOL; // 初期状態を設定します。 TRUE - // シグナル状態のオブジェクト lpName: PChar // 名前、または名前が必要ない場合は NIL): THandle; 標準呼び出し; // 作成されたオブジェクトの識別子を返します。 TSecurityAttributes 構造体は次のように記述されます。 TSecurityAttributes = record nLength: DWORD; // 構造体のサイズは // SizeOf(TSecurityAttributes) lpSecurityDescriptor: Pointer; として初期化する必要があります。 // セキュリティ記述子のアドレス。 // Windows 95 および 98 では無視されます。 // 通常は NIL を指定できます。 bInheritHandle: BOOL; // 子プロセスが // オブジェクト end を継承できるかどうかを設定します。

Windows NT で特別なアクセス権を設定する必要がない場合、または子プロセスがオブジェクトを継承する機能を設定する必要がない場合は、lpEventAttributes パラメータとして NIL を渡すことができます。 この場合、オブジェクトは子プロセスに継承できず、「デフォルト」セキュリティ記述子が与えられます。

lpName パラメータを使用すると、プロセス間でオブジェクトを共有できます。 lpName が、現在のプロセスまたは他のプロセスによって作成されたタイプ Event の既存のオブジェクトの名前と一致する場合、関数は新しいオブジェクトを作成せず、既存のオブジェクトの識別子を返します。 これは、bManualReset、bInitialState、および lpSecurityDescriptor パラメーターを無視します。 次のように、オブジェクトが作成されたか、既存のオブジェクトが使用されているかを確認できます。

HEvent:= CreateEvent(NIL, TRUE, FALSE, 'イベント名');

hEvent = 0 の場合は RaiseLastWin32Error;

if GetLastError = ERROR_ALREADY_EXISTS then begin // 以前に作成したオブジェクトを使用します end;

オブジェクトが単一プロセス内の同期に使用される場合、そのオブジェクトをグローバル変数として宣言し、名前なしで作成できます。

オブジェクト名は、既存のセマフォ、ミューテックス、ジョブ、待機可能タイマー、またはファイルマッピング オブジェクトの名前と同じであってはなりません。 名前が一致する場合、関数はエラーを返します。

イベントがすでに作成されていることがわかっている場合は、CreateEvent の代わりに OpenEvent 関数を使用してそれにアクセスできます。

Function OpenEvent(dwDesiredAccess: DWORD; // オブジェクトへのアクセス権を設定します。 bInheritHandle: BOOL; // オブジェクトが子プロセスによって継承できるかどうかを設定します。 // lpName: PChar // オブジェクト名): THandle; 標準呼び出し;

この関数はオブジェクト識別子を返すか、エラーの場合は 0 を返します。 dwDesiredAccess パラメーターには、次のいずれかの値を指定できます。

識別子を受け取ったら、使用を開始できます。 これには次の関数が使用できます。

関数 SetEvent(hEvent: THandle): BOOL; 標準呼び出し;

オブジェクトをアラーム状態に設定します

関数 ResetEvent(hEvent: THandle): BOOL; 標準呼び出し;

オブジェクトをリセットして、信号のない状態にします。

関数 PulseEvent(hEvent: THandle): BOOL; 標準呼び出し

Var Events: THandle の配列。 // 同期オブジェクトの配列 Overlapped: TOverlapped の配列;

... // 同期オブジェクトを作成 Events := CreateEvent(NIL, TRUE, FALSE, NIL);

イベント := CreateEvent(NIL, TRUE, FALSE, NIL);

// 構造体を初期化します TOverlapped FillChar(Overlapped, SizeOf(Overlapped), 0);

Overlapped.hEvent:= イベント;

Overlapped.hEvent:= イベント;

// ファイルへの非同期書き込みを開始します WriteFile(hFirstFile, FirstBuffer, SizeOf(FirstBuffer), FirstFileWritten, @Overlapped);

WriteFile(hSecondFile, SecondBuffer, SizeOf(SecondBuffer), SecondFileWritten, @Overlapped);

// 両ファイルへの書き込み完了を待つ WaitForMultipleObjects(2, @Events, TRUE, INFINITE);

この関数は、作成されたオブジェクトの識別子または 0 を返します。指定された名前のミューテックスがすでに作成されている場合は、その識別子が返されます。 この場合、GetLastError 関数はエラー コード ERROR_ALREDY_EXISTS を返します。 この名前は、既存のセマフォ、イベント、ジョブ、待機可能タイマー、または FileMapping オブジェクトの名前と同じであってはなりません。

同じ名前のミューテックスがすでに存在するかどうかが不明な場合、プログラムは作成時にオブジェクトの所有権を要求してはなりません (つまり、bInitialOwner を FALSE として渡す必要があります)。

ミューテックスがすでに存在する場合、アプリケーションは OpenMutex 関数を使用してその識別子を取得できます。

Function OpenMutex(dwDesiredAccess: DWORD; // オブジェクトへのアクセス権を設定します。 bInheritHandle: BOOL; // オブジェクトが子プロセスによって継承できるかどうかを設定します。 // lpName: PChar // オブジェクト名): THandle; 標準呼び出し;

この関数は、開いているミューテックスの識別子を返すか、エラーの場合は 0 を返します。 ミューテックスは、その識別子が渡された wait 関数が起動した後、シグナル状態に入ります。 非シグナル状態に戻すには、ReleaseMutex 関数を使用します。

関数 ReleaseMutex(hMutex:THandle): BOOL; 標準呼び出し;

複数のプロセスが、たとえばメモリマップされたファイルを通じて通信している場合、共有リソースへの正しいアクセスを保証するために、各プロセスに次のコードが含まれている必要があります。

Var Mutex: THandle;

// Mutex プログラムを初期化するとき:= CreateMutex(NIL, FALSE, 'UniqueMutexName');

もちろん、リソースの操作にかなりの時間がかかる可能性がある場合は、MsgWaitForSingleObject 関数を使用するか、ゼロ タイムアウト ループで WaitForSingleObject を呼び出して戻りコードを確認する必要があります。 そうしないと、アプリケーションがフリーズします。 try ...finally ブロックを使用して同期オブジェクトの取得と解放を常に保護してください。そうしないと、リソースの操作中にエラーが発生し、リソースの解放を待っているすべてのプロセスがブロックされます。

セマフォ (セマフォ)

セマフォは、0 から作成時に指定された最大値までの範囲の整数を含むカウンターです。 このカウンタは、スレッドがセマフォを使用する待機関数を正常に完了するたびに減分され、ReleaseSemaphore 関数を呼び出すことによって増分されます。 セマフォが値 0 に達すると非シグナリング状態になり、その他のカウンタ値ではその状態がシグナリングされます。 この動作により、事前定義された数の接続をサポートするリソースへのアクセスの制限としてセマフォを使用できるようになります。

セマフォを作成するには、CreateSemaphore 関数を使用します。

Function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes; // 構造体アドレス // TSecurityAttributes lInitialCount, // 初期カウンタ値 lMinimumCount: Longint; // 最大カウンタ値 lpName: PChar // オブジェクト名): THandle; 標準呼び出し;

この関数は、作成されたセマフォの識別子を返すか、オブジェクトを作成できなかった場合は 0 を返します。

lMinimumCount パラメータはセマフォ カウンタの最大値を指定し、lInitialCount はカウンタの初期値を指定します。これは 0 から lMinimumCount の範囲内である必要があります。 lpName はセマフォの名前を指定します。 システムに同じ名前のセマフォがすでにある場合、新しいセマフォは作成されませんが、既存のセマフォの識別子が返されます。 セマフォが 1 つのプロセス内で使用される場合、NIL を lpName として渡すことで、名前なしでセマフォを作成できます。 セマフォ名は、既存のイベント、ミューテックス、待機可能タイマー、ジョブ、またはファイル マッピング オブジェクトの名前と同じであってはなりません。

以前に作成したセマフォの ID は、OpenSemaphore 関数によって取得することもできます。

Function OpenSemaphore(dwDesiredAccess: DWORD; // オブジェクトへのアクセス権を設定します。 bInheritHandle: BOOL; // オブジェクトが子プロセスによって継承できるかどうかを設定します。 // lpName: PChar // オブジェクト名): THandle; 標準呼び出し;

dwDesiredAccess パラメーターには、次のいずれかの値を指定できます。

セマフォ カウンタを増やすには、ReleaseSemaphore 関数を使用します。

Function ReleaseSemaphore(hSemaphore: THandle; // セマフォ識別子 lReleaseCount: Longint; // カウンタはこの値によってインクリメントされます lpPreviousCount: Pointer // // カウンタの前の値を取得する 32 ビット変数のアドレス //) : ブール値; 標準呼び出し;

関数の実行後のカウンタ値が CreateSemaphore 関数で指定された最大値を超えた場合、ReleaseSemaphore は FALSE を返し、セマフォ値は変更されません。 この値が必要ない場合は、NIL を lpPreviousCount パラメータとして渡すことができます。

複数のタスクを別々のスレッドで実行するアプリケーションの例を考えてみましょう (たとえば、インターネットからファイルをバックグラウンドでダウンロードするプログラム)。 同時に実行するタスクの数が多すぎると、チャネルに不要な負荷がかかります。 したがって、タスクが実行されるスレッドを、その数が所定の値を超えた場合にスレッドが停止し、前に実行されていたタスクが完了するまで待機するように実装します。

ユニット限定スレッド。

インターフェースはクラスを使用します。

type TLimitedThread = class(TThread) プロシージャ 実行; オーバーライド; 終わり; 実装では Windows を使用します。

const MAX_THREAD_COUNT = 10;< 5; i++) { Thread myThread = new Thread(Count); myThread.Name = "Поток " + i.ToString(); myThread.Start(); } Console.ReadLine(); } public static void Count() { x = 1; for (int i = 1; i < 9; i++) { Console.WriteLine("{0}: {1}", Thread.CurrentThread.Name, x); x++; Thread.Sleep(100); } } }

ここでは、共通の変数 x を処理する 5 つのスレッドが実行されています。 そして、メソッドが 1 から 8 までのすべての x 値を出力すると仮定します。以下同様にスレッドごとに同様です。 しかし、実際には動作中にスレッド間の切り替えが発生し、変数 x の値が予測不能になります。

この問題の解決策は、スレッドを同期し、一部のスレッドによって使用されている共有リソースへのアクセスを制限することです。 これには、lock キーワードが使用されます。 lock ステートメントは、すべてのコードがロックされ、現在のスレッドが終了するまで他のスレッドが使用できなくなるコード ブロックを定義します。 そして、前の例を次のように作り直すことができます。

クラス プログラム ( static int x=0; static オブジェクト ロッカー = new object(); static void Main(string args) (for (int i = 0; i< 5; i++) { Thread myThread = new Thread(Count); myThread.Name = "Поток " + i.ToString(); myThread.Start(); } Console.ReadLine(); } public static void Count() { lock (locker) { x = 1; for (int i = 1; i < 9; i++) { Console.WriteLine("{0}: {1}", Thread.CurrentThread.Name, x); x++; Thread.Sleep(100); } } } }

でロックするには キーワード lock はスタブ オブジェクト (この場合はロッカー変数) を使用します。 実行が lock ステートメントに到達すると、ロッカー オブジェクトはロックされ、ロックされている間は 1 つのスレッドのみがコード ブロックに排他的にアクセスできます。 コードのブロックが終了すると、ロッカー オブジェクトが解放され、他のスレッドで使用できるようになります。

記事の前の部分では、マルチスレッド アプリケーションを構築するための一般原則と具体的な方法について説明しました。 異なるスレッドはほとんどの場合、定期的に相互作用する必要があり、必然的に同期の必要性が生じます。 今日は、最も重要で、最も強力で、最も多用途な Windows 同期ツールであるカーネル同期オブジェクトについて見ていきます。

WaitForMultipleObjects およびその他の待機関数

ご存知のとおり、スレッドを同期するには、通常、いずれかのスレッドの実行を一時的に中断する必要があります。 同時に、オペレーティング システムによって、プロセッサ時間を消費しないスタンバイ状態に移行する必要があります。 これを実行できる 2 つの関数、SuspendThread と ResumeThread がすでにわかっています。 ただし、記事の前の部分で述べたように、いくつかの機能により、これらの関数は同期には適していません。

今日は別の関数を見ていきます。この関数もスレッドを待機状態にしますが、 SuspendThread/ResumeThread とは異なり、特に同期を組織するために設計されています。 これは WaitForMultipleObjects です。 この関数は非常に重要であるため、API の詳細には触れないという私のルールから少し逸脱し、そのプロトタイプも提供しながら、より詳細に説明します。

DWORD WaitForMultipleObjects (

DWORD nCount , // lpHandles 配列内のオブジェクトの数

定数ハンドル * lpハンドル , // カーネル オブジェクト記述子の配列へのポインタ

BOOL bWaitAll , // すべてのオブジェクトを待機する必要があるか、1 つだけで十分であるかを示すフラグ

DWORD dwミリ秒 // タイムアウト

この関数の主なパラメータは、カーネル オブジェクト ハンドルの配列へのポインタです。 これらのオブジェクトが何であるかについては、以下で説明します。 現時点では、これらのオブジェクトはいずれも、ニュートラルまたは「シグナル」(シグナル状態) の 2 つの状態のいずれかになる可能性があることを知っておくことが重要です。 bWaitAll フラグが FALSE の場合、関数はオブジェクトの少なくとも 1 つがシグナルを発するとすぐに制御を返します。 そして、フラグが TRUE の場合、これはすべてのオブジェクトが同時にシグナル送信を開始した場合にのみ発生します (後で説明するように、これはこの関数の最も重要なプロパティです)。 最初のケースでは、戻り値によって、どのオブジェクトがシグナルを送信したかを知ることができます。 そこから定数 WAIT_OBJECT_0 を減算する必要があり、lpHandles 配列のインデックスを取得します。 待機時間が最後のパラメータで指定されたタイムアウトを超える場合、関数は待機を停止し、値 WAIT_TIMEOUT を返します。 タイムアウトとして定数 INFINITE を指定すると、関数は「ずっと」待機することになり、逆に 0 を指定すると、スレッドはまったく中断されなくなります。 後者の場合、関数はすぐに制御を返しますが、その結果からオブジェクトの状態を知ることができます。 最後のテクニックは非常に頻繁に使用されます。 ご覧のとおり、この関数には豊富な機能があります。 他にも WaitForXXX 関数がいくつかありますが、それらはすべてメイン テーマのバリエーションです。 特に、WaitForSingleObject はその単純化されたバージョンにすぎません。 残りの部分にはそれぞれ独自の追加機能がありますが、一般に使用される頻度は低くなります。 たとえば、カーネル オブジェクトからの信号だけでなく、スレッドのキュー内の新しいウィンドウ メッセージの到着にも応答できるようになります。 いつものように、WaitForMultipleObjects の説明と詳細情報は MSDN で見つけることができます。

ここで、これらの謎の「カーネル オブジェクト」が何であるかについて説明しましょう。 これらにはスレッドとプロセス自体が含まれるという事実から始めましょう。 完了するとすぐにシグナル状態になります。 多くの場合、スレッドまたはプロセスがいつ終了するかを追跡する必要があるため、これは非常に重要な機能です。 たとえば、一連のワーカー スレッドを備えたサーバー アプリケーションを終了する必要があるとします。 この場合、制御スレッドは何らかの方法でワーカー スレッドに作業を終了する時間であることを通知し (たとえば、グローバル フラグを設定することによって)、すべてのスレッドが完了するまで待機し、正しく完了するために必要なすべてのアクション (リソースの解放) を実行する必要があります。 、シャットダウンやネットワーク接続の切断などについてクライアントに通知します。

スレッドが作業を終了するとシグナルをオンにするという事実により、スレッド終了との同期の問題を非常に簡単に解決できます。

// 簡単にするために、ワーカー スレッドを 1 つだけにします。 起動しましょう:

ハンドル hWorkerThread = :: スレッドの作成 (...);

// 作業を終了する前に、何らかの方法でワーカー スレッドにダウンロードの時期が来たことを通知する必要があります。

// スレッドが完了するまで待ちます。

DWORD dwWaitResult = :: シングルオブジェクトを待つ ( hワーカースレッド , 無限 );

もし( dwWaitResult != WAIT_OBJECT_0 ) { /* エラー処理 */ }

// スレッド ハンドルを閉じることができます。

確認する (:: クローズハンドル ( hワーカースレッド );

/* CloseHandle が失敗して FALSE を返した場合、例外はスローされません。 まず、たとえシステム エラーが原因でこれが起こったとしても、プログラムに直接的な影響はありません。ハンドルを閉じているため、それ以上の作業は期待されていないことを意味します。 実際には、CloseHandle の失敗はプログラム内のエラーを意味するだけです。 そこで、アプリケーションのデバッグ段階で見逃さないように、ここにVERIFYマクロを挿入します。 */

プロセスが完了するのを待っているコードは同様に見えます。

システムにそのような機能が組み込まれていない場合、ワーカー スレッド自体が何らかの方法でその完了に関する情報をメイン スレッドに送信する必要があります。 たとえこれを最後に実行したとしても、メインスレッドは、ワーカーに実行すべきアセンブリ命令が少なくとも 2 つ残っていないことを確信できません。 特定の状況では (たとえば、スレッドのコードが終了時にアンロードする必要がある DLL 内にある場合)、これは致命的な結果につながる可能性があります。

スレッド (またはプロセス) が終了した後でも、そのハンドルは CloseHandle 関数で明示的に閉じられるまで有効なままであることに注意してください。 (ちなみに、これを行うことを忘れないようにしてください。) これは、スレッドの状態をいつでも確認できるようにするために行われます。

したがって、WaitForMultipleObjects 関数 (およびその類似関数) を使用すると、スレッドの実行を同期オブジェクト (特に他のスレッドやプロセス) の状態と同期させることができます。

カーネル特殊オブジェクト

次に、同期専用に設計されたカーネル オブジェクトについて考えてみましょう。 これらはイベント、セマフォ、ミューテックスです。 それぞれを簡単に見てみましょう。

イベント

おそらく最も単純で最も基本的な同期オブジェクトです。 これは、SetEvent/ResetEvent 関数で設定できる単なるフラグです (シグナリングまたはニュートラル)。 イベントは、待機中のスレッドに何らかのイベントが発生し (そのためそう呼ばれる) 信号を送信し、動作を継続できる最も便利な方法です。 イベントを使用すると、ワーカー スレッドを初期化する際の同期の問題を簡単に解決できます。

// 簡単にするために、イベント ハンドルをグローバル変数に格納します。

ハンドル g_hEventInitComplete = NULL ; // 変数を初期化しないままにしないでください。

{ // メインスレッド内のコード

// イベントを作成する

g_hEventInitComplete = :: イベントの作成 ( NULL、

間違い , // このパラメータについては後で説明します

間違い , // 初期状態 - ニュートラル

もし(! g_hEventInitComplete ) { /* エラー処理を忘れないでください */ }

// ワーカースレッドを作成する

DWORD idWorkerThread = 0 ;

ハンドル hWorkerThread = :: スレッドの作成 ( NULL , 0 , & WorkerThreadProc , NULL , 0 , & idWorkerThread );

もし(! hワーカースレッド ) { /* エラー処理 */ }

// ワーカースレッドからのシグナルを待ちます

DWORD dwWaitResult = :: シングルオブジェクトを待つ ( g_hEventInitComplete , 無限 );

もし( dwWaitResult != WAIT_OBJECT_0 ) { /* エラー */ }

// これで、ワーカー スレッドが初期化を完了したことを確認できます。

確認する (:: クローズハンドル ( g_hEventInitComplete )); // 不要なオブジェクトを閉じることを忘れないでください

g_hEventInitComplete = NULL ;

// ワーカースレッド関数

DWORD WINAPI WorkerThreadProc ( LPVOID_パラメータ )

ワーカーの初期化 (); // 初期化

// 初期化が完了したことを知らせます

ブールはOKです = :: イベントの設定 ( g_hEventInitComplete );

もし(! 大丈夫です ) { /* エラー */ }

2 つの著しく異なるタイプのイベントがあることに注意してください。 CreateEvent 関数の 2 番目のパラメーターを使用して、それらの 1 つを選択できます。 TRUE の場合、状態が手動、つまり SetEvent/ResetEvent 関数によってのみ制御されるイベントが作成されます。 FALSE の場合、自動リセット イベントが生成されます。 これは、特定のイベントを待機しているスレッドがそのイベントからのシグナルによって解放されるとすぐに、自動的にニュートラル状態にリセットされることを意味します。 それらの違いは、複数のスレッドが 1 つのイベントを同時に待機している状況で最も顕著になります。 手動イベントはスタート号砲のようなものです。 シグナリング状態に設定されると、すべてのスレッドが一度に解放されます。 自動リセット イベントは地下鉄の回転式改札口に似ており、1 つのフローのみを解放し、ニュートラル状態に戻ります。

ミューテックス

イベントと比較すると、より特殊なオブジェクトです。 通常、複数のスレッドで共有されるリソースへのアクセスなど、一般的な同期の問題を解決するために使用されます。 多くの点で、これは自動リセット イベントに似ています。 主な違いは、特定のスレッドへの特別なバインドがあることです。 ミューテックスがシグナル状態にある場合、それはフリーであり、どのスレッドにも属していないことを意味します。 特定のスレッドがこのミューテックスを待機するとすぐに、後者はニュートラル状態にリセットされ (ここでは自動リセット イベントと同様です)、スレッドは ReleaseMutex 関数で明示的にミューテックスを解放するまでその所有者になります。または終了します。 したがって、一度に 1 つのスレッドだけが共有データを処理していることを確認するには、そのような作業が発生するすべての場所を次のペアで囲む必要があります。 WaitFor - ReleaseMutex :

ハンドル g_hMutex ;

// ミューテックス ハンドルをグローバル変数に格納します。 もちろん、ワーカー スレッドを開始する前に、事前に作成しておく必要があります。 これはすでに行われていると仮定しましょう。

整数待つ = :: シングルオブジェクトを待つ ( g_hMutex , 無限 );

スイッチ(待つ ) {

場合 WAIT_OBJECT_0 : // すべて問題ありません

壊す;

場合待機放棄しました : /* 一部のスレッドが終了しました。ReleaseMutex の呼び出しを忘れています。 おそらく、これはプログラムにバグがあることを意味します。 したがって、念のためここに ASSERT を挿入しますが、最終バージョン (リリース) では、このコードは成功したものとみなされます。 */

アサート ( 間違い );

壊す;

デフォルト:

// ここにはエラー処理が必要です。

// ミューテックスによって保護されたコードのセクション。

プロセス共通データ ();

確認する (:: リリースミューテックス ( g_hMutex ));

ミューテックスが自動リセット イベントよりも優れているのはなぜですか? 上記の例では、これも使用できますが、 ReleaseMutex のみを SetEvent に置き換える必要があります。 しかしながら、次のような困難が生じる可能性がある。 ほとんどの場合、複数の場所で共有データを操作する必要があります。 この例の ProcessCommonData が、同じデータを処理し、すでに独自の WaitFor - ReleaseMutex ペアを持っている関数を呼び出した場合はどうなりますか (実際には、これは非常に頻繁に起こります)。 イベントを使用すると、保護されたブロック内ではイベントがニュートラル状態にあるため、プログラムは明らかにハングします。 ミューテックスはより巧妙に設計されています。 ホスト スレッドの場合は、他のすべてのスレッドがニュートラルであるにもかかわらず、常にシグナル状態のままです。 したがって、スレッドがミューテックスを取得した場合、再度 WaitFor 関数を呼び出してもブロックされません。 さらに、ミューテックスにはカウンターも組み込まれているため、WaitFor が呼び出されたのと同じ回数だけ ReleaseMutex を呼び出す必要があります。 このようにして、コードが再帰的に呼び出されることを心配することなく、WaitFor - ReleaseMute x ペアを使用して共有データを操作するすべてのコードを安全に保護できます。 これにより、ミューテックスは非常に使いやすいツールになります。

セマフォ

さらに具体的な同期オブジェクト。 私の実践において、それが役に立ったケースは一度もなかったことを認めなければなりません。 セマフォは、特定のリソースで同時に動作できるスレッドの最大数を制限するように設計されています。 基本的に、セマフォはカウンターを持つイベントです。 このカウンタがゼロより大きい限り、セマフォはシグナリング状態になります。 ただし、WaitFor を呼び出すたびに、このカウンタは 1 ずつ減少し、ゼロになり、セマフォがニュートラル状態になります。 ミューテックスと同様に、セマフォにはカウンターをインクリメントする ReleaseSemaphor 関数があります。 ただし、ミューテックスとは異なり、セマフォはスレッドに関連付けられていないため、WaitFor/ReleaseSemaphor を再度呼び出すと、カウンターが再び減少または増加します。

セマフォはどのように使用できますか? たとえば、マルチスレッドを人為的に制限するために使用できます。 すでに説明したように、同時にアクティブなスレッドが多すぎると、コンテキストの切り替えが頻繁に行われるため、システム全体のパフォーマンスが大幅に低下する可能性があります。 また、作成するワーカー スレッドの数が多すぎる場合は、同時にアクティブなスレッドの数をプロセッサの数程度に制限できます。

カーネル同期オブジェクトについて他に何か言えるでしょうか? 名前を付けることができるととても便利です。 同期オブジェクトを作成するすべての関数には、対応するパラメーター (CreateEvent、CreateMutex、CreateSemaphore) があります。 たとえば、CreateEvent を 2 回呼び出し、両方とも空ではない同じ名前を指定した場合、2 回目では、関数は新しいオブジェクトを作成する代わりに、既存のオブジェクトのハンドルを返します。 これは、2 番目の呼び出しが別のプロセスから行われた場合でも発生します。 後者は、異なるプロセスに属するスレッドを同期する必要がある場合に非常に便利です。

同期オブジェクトが必要なくなったら、スレッドについて説明したときにすでに述べた CloseHandle 関数を呼び出すことを忘れないでください。 実際、オブジェクトがすぐに削除されるとは限りません。 実際には、オブジェクトには複数のハンドルを持つことができ、最後のハンドルが閉じられた場合にのみ削除されます。

思い出してもらいたいのですが 最良の方法緊急事態が発生した場合でも、CloseHandle または同様の「クリーンアップ」関数が確実に呼び出されるようにするには、それをデストラクターに配置します。 ちなみに、これについてはかつてキリル・プレシフツェフの記事「スマート・デストラクター」で非常に詳しく詳しく説明されていました。 上記の例では、API 関数の操作をより明確にするために、この手法を教育目的のみに使用しませんでした。 実際のコードでは、クリーンアップのためにスマート デストラクターを備えたラッパー クラスを常に使用する必要があります。

ちなみに、 ReleaseMutex 関数や類似の関数でも、 CloseHandle と同じ問題が常に発生します。 この作業がどれほど正常に完了したかに関係なく (結局のところ、例外がスローされた可能性があります)、一般データを使用した作業の完了時に呼び出される必要があります。 ここでは「物忘れ」の影響がより深刻です。 CloseHandle を呼び出さないとリソース リークが発生するだけの場合 (これも問題です!)、解放されていないミューテックスにより、失敗したスレッドが終了するまで他のスレッドが共有リソースを操作できなくなり、アプリケーションが機能できなくなる可能性が高くなります。普通に。 これを回避するには、スマート デストラクターを備えた特別に訓練されたクラスが再び役に立ちます。

同期オブジェクトのレビューの最後に、Win32 API にないオブジェクトについて触れたいと思います。 私の同僚の多くは、Win32 に「1 つの書き込み、多数の読み取り」タイプの特殊なオブジェクトがないのはなぜかと困惑しています。 これは一種の「高度なミューテックス」で、一度に 1 つのスレッドだけが書き込み目的で共通データにアクセスでき、複数のスレッドが読み取り専用で共通データにアクセスできるようにします。 同様のオブジェクトが UNIX にもありますが、Borland などの一部のライブラリでは、標準の同期オブジェクトに基づいてエミュレートすることができます。ただし、そのようなオブジェクトが実際に実行できるかどうかは非常に疑わしいです。ただし、Windows カーネルではそのようなオブジェクトは提供されません。

なぜ Windows NT カーネル開発者はこれに対処しなかったのでしょうか? なぜUNIXよりも劣っているのでしょうか? 私の考えでは、その答えは、Windows ではそのようなオブジェクトに対する実際のニーズがまだ存在していないということです。 通常の単一プロセッサ マシンでは、スレッドが物理的に同時に動作できないため、ミューテックスとほぼ同等になります。 マルチプロセッサ マシンでは、読み取りスレッドが並行して動作できるため、利点が得られます。 同時に、この利点は、読み取りスレッドの「衝突」の可能性が高い場合にのみ実際に顕著になります。 たとえば、1024 プロセッサのマシンでは、このようなカーネル オブジェクトが不可欠であることは疑いの余地がありません。 同様のマシンは存在しますが、これらは特殊な OS で実行される特殊なシステムです。 多くの場合、このようなオペレーティング システムは UNIX をベースに構築されており、おそらくそこから「1 回の書き込み、多数の読み取り」タイプのオブジェクトがこのシステムのより一般的に使用されるバージョンに導入されました。 しかし、私たちが慣れ親しんでいる x86 マシンでは、通常、プロセッサーが 1 つだけ、場合によっては 2 つだけプロセッサーがインストールされます。 また、Intel Xeon などのプロセッサの最新モデルのみが 4 つ以上のプロセッサ構成をサポートしていますが、そのようなシステムは依然として珍しいものです。 しかし、そのような「高度な」システムであっても、「高度なミューテックス」は非常に特殊な状況でのみ顕著なパフォーマンスの向上を実現できます。

したがって、「高度な」ミューテックスを実装するのは、手間をかける価値がまったくありません。 「低プロセッサ」マシンでは、標準のミューテックスと比較してオブジェクトのロジックが複雑なため、効果がさらに低くなる可能性があります。 このようなオブジェクトの実装は、一見したほど単純ではないことに注意してください。 実装が失敗した場合、読み取りスレッドが多すぎる場合、書き込みスレッドは単にデータに「到達できません」。 これらの理由から、そのようなオブジェクトをエミュレートしようとすることはお勧めしません。 実際のマシン上の実際のアプリケーションでは、通常のミューテックスまたはクリティカル セクション (この記事の次の部分で説明します) が、共有データへのアクセスを同期するという優れた機能を果たします。 ただし、Windows OS の発展に伴い、遅かれ早かれ「1 回の書き込みで多数の読み取りを行う」カーネル オブジェクトが登場すると思います。

注記。 実際、「1 回の書き込み、多数の読み取り」オブジェクトは Windows NT にもまだ存在します。 この記事を書いた時点では知りませんでした。 このオブジェクトは「カーネル リソース」と呼ばれ、ユーザー モード プログラムからはアクセスできないため、おそらくあまり知られていません。 これに関する同様の情報が DDK にあります。 これを指摘してくれた Konstantin Manurin に感謝します。

デッドロック

ここで、WaitForMultipleObjects 関数、より正確には 3 番目のパラメーターである bWaitAll に戻りましょう。 複数のオブジェクトを同時に待機できる機能がなぜそれほど重要なのかを説明することを約束しました。

複数のオブジェクトのうちの 1 つを待機できる関数が必要な理由は明らかです。 特別な関数がない場合、これは空のループでオブジェクトの状態を順番にチェックすることによってのみ行うことができますが、もちろんこれは受け入れられません。 しかし、複数のオブジェクトが同時に信号状態になる瞬間を待つことができる特別な関数の必要性は、それほど明白ではありません。 実際に、次のような典型的な状況を想像してみましょう。 ある瞬間一度に 2 つの共有データセットにアクセスする必要があります。それぞれが独自のミューテックスを担当し、それらを A と B と呼びます。スレッドは、最初にミューテックス A が解放されるまで待機し、それを取得し、次に待機することができるように見えます。ミューテックス B を解放します... WaitForSingleObject を数回呼び出すだけで済むようです。 実際、これは機能しますが、他のすべてのスレッドが最初に A、次に B という同じ順序でミューテックスを取得している限りに限られます。スレッドが逆のこと、つまり最初に B を取得し、次に A を取得しようとするとどうなるでしょうか。 遅かれ早かれ、1 つのスレッドがミューテックス A をキャプチャし、別のスレッドがミューテックス B をキャプチャし、最初のスレッドが B が解放されるのを待機し、2 番目のスレッドがミューテックス A をキャプチャするという状況が発生します。これらのスレッドがこれを決して待機しないことは明らかであり、プログラムはフリーズします。

この種のデッドロックは非常に一般的なエラーです。 同期に関連するすべてのエラーと同様、このエラーは時々しか発生せず、プログラマーの神経をすり減らす可能性があります。 同時に、複数の同期オブジェクトを伴うほぼすべてのスキームにはデッドロックが伴います。 したがって、このような回路の設計段階では、この問題に特別な注意を払う必要があります。

上記の単純な例では、ブロックを回避するのは非常に簡単です。 すべてのスレッドが特定の順序 (最初に A、次に B) でミューテックスを取得することを要求する必要があります。ただし、多くのオブジェクトがさまざまな方法で相互に接続されている複雑なプログラムでは、通常、これを達成するのはそれほど簡単ではありません。 2 つではなく、多くのオブジェクトとスレッドがロックに関与する可能性があります。 したがって、スレッドが一度に複数の同期オブジェクトを必要とする状況でデッドロックを回避する最も確実な方法は、bWaitAll=TRUE パラメーターを指定した WaitForMultipleObjects 関数への 1 回の呼び出しですべての同期オブジェクトを取得することです。 実際のところ、この場合、デッドロックの問題をオペレーティング システム カーネルに移しているだけですが、重要なことは、これが私たちの懸念ではなくなるということです。 ただし、多くのオブジェクトを含む複雑なプログラムでは、特定の操作を実行するためにどのオブジェクトが必要になるかをすぐに判断できない場合があり、すべての WaitFor 呼び出しを 1 か所にまとめて結合するのは容易ではないことがよくあります。

したがって、デッドロックを回避するには 2 つの方法があります。 同期オブジェクトがスレッドによって常にまったく同じ順序で取得されるようにするか、 WaitForMultipleObjects への 1 回の呼び出しで同期オブジェクトが取得されるようにする必要があります。 後者の方法が簡単で好ましい。 ただし、実際には、両方の要件を満たすには常に困難が生じるため、両方のアプローチを組み合わせる必要があります。 複雑なタイミング回路の設計は、多くの場合、非常に困難な作業です。

同期編成例

上で説明したようなほとんどの一般的な状況では、同期を構成することは難しくありません。イベントまたはミューテックスだけで十分です。 しかし、問題の解決策がそれほど明らかではない、より複雑なケースが時々発生します。 私の実践からの具体的な例でこれを説明したいと思います。 ご覧のとおり、解決策は驚くほど簡単でしたが、解決策を見つけるまでにいくつかの失敗したオプションを試さなければなりませんでした。

ということで、タスクです。 ほとんどすべての最新のダウンロード マネージャー (簡単に言うと「ロッキング チェア」) には、バックグラウンドで実行されている「ロッキング チェア」がユーザーのインターネット サーフィンを大きく妨げないようにトラフィックを制限する機能があります。 私も同様のプログラムを開発しており、まさにそのような「機能」を実装するという任務を与えられました。 私のダウンローダーは、各タスク (この場合は特定のファイルのダウンロード) が別個のスレッドで処理される、古典的なマルチスレッド スキームに従って動作しました。 トラフィック制限はすべてのフローに対して累積される必要があります。 つまり、一定の時間間隔中に、すべてのスレッドがソケットから特定のバイト数を超えて読み取らないようにする必要がありました。 ファイルのダウンロードが非常に不均一になる可能性があり、一方のダウンロードは速く、他方のダウンロードは遅くなる可能性があるため、この制限を単純にストリーム間で均等に分割することは明らかに効果的ではありません。 したがって、すべてのスレッドに共通のカウンター、読み取られたバイト数、およびさらに読み取れるバイト数が必要です。 ここで同期が不可欠になります。 ワーカー スレッドのいずれかをいつでも停止できるという要件により、タスクはさらに複雑になります。

問題をさらに詳しく定式化してみましょう。 同期システムを特別なクラスに含めることにしました。 そのインターフェースは次のとおりです。

クラス Cクォータ {

公共: // メソッド

空所セット ( 符号なし整数 _nクォータ );

符号なし整数リクエスト ( 符号なし整数 _nBytesToRead , HANDLE_hStopEvent );

空所リリース ( 符号なし整数 _nBytes元に戻す , HANDLE_hStopEvent );

定期的に (たとえば 1 秒に 1 回)、制御スレッドは Set メソッドを呼び出し、ダウンロード クォータを設定します。 ワーカー スレッドは、ネットワークから受信したデータを読み取る前に、Request メソッドを呼び出します。このメソッドは、現在のクォータがゼロでないことを確認し、ゼロである場合は、現在のクォータ内で読み取ることができるバイト数を返します。 それに応じて、クォータもこの数だけ減ります。 Request が呼び出されたときにクォータが 0 の場合、呼び出しスレッドはクォータが表示されるまで待機する必要があります。 要求されたバイト数よりも実際に受信されるバイト数が少ない場合があります。その場合、スレッドは Release メソッドを使用して、スレッドに割り当てられたクォータの一部を返します。 そして、すでに述べたように、ユーザーはいつでもダウンロードを停止するコマンドを与えることができます。 この場合、クォータの存在に関係なく、待機を中断する必要があります。 これには特別なイベント _hStopEvent が使用されます。 タスクは互いに独立して開始および停止できるため、各ワーカー スレッドには独自の停止イベントがあります。 そのハンドルは Request メソッドと Release メソッドに渡されます。

失敗したオプションの 1 つとして、CQuota クラスへのアクセスを同期するミューテックスと、クォータの存在を通知するイベントを組み合わせて使用​​しようとしました。 ただし、停止イベントはこのスキームには当てはまりません。 スレッドがクォータを取得したい場合は、その待機状態を複雑なブール式 ((ミューテックス AND クォータ イベント) OR 停止イベント) によって制御する必要があります。 しかし、WaitForMultipleObjects ではこれが許可されていません。複数のカーネル オブジェクトを AND または OR 演算で結合することはできますが、混合することはできません。 WaitForMultipleObjects を 2 回連続して呼び出して待機を分割しようとすると、必然的にデッドロックが発生します。 一般的に、この道は行き止まりであることが判明しました。

これ以上霧を生じさせず、解決策を教えます。 前に述べたように、ミューテックスは自動リセット イベントに非常に似ています。 ここでは、1 つだけではなく 2 つを同時に使用した方が便利な、まれなケースを示します。

クラス Cクォータ {

プライベート: // データ

符号なし整数 m_nクォータ ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

これらのイベントは一度に 1 つだけ設定できます。 クォータを操作するスレッドは、残りのクォータがゼロ以外の場合に最初のイベントを発生させ、クォータが使い果たされた場合に 2 番目のイベントを発生させる必要があります。 クォータを取得したいスレッドは、最初のイベントを待つ必要があります。 クォータを増加させるスレッドは、これらのイベントのいずれかを待つだけで済みます。両方のイベントがリセット状態にある場合、これは別のスレッドが現在クォータを処理していることを意味するためです。 したがって、2 つのイベントは、データ アクセスの同期と待機という 2 つの機能を同時に実行します。 最後に、スレッドは 2 つのイベントのいずれかを待機しているため、スレッドに停止を通知するイベントを簡単に含めることができます。

Request メソッドの実装例を示します。 残りも同様に実装されます。 実際のプロジェクトで使用されるコードを少し簡略化しました。

符号なし整数 Cクォータ :: リクエスト ( 符号なし整数 _nリクエスト , HANDLE_hStopEvent )

もし(! _nリクエスト ) 戻る 0 ;

符号なし整数 n提供する = 0 ;

hイベントのハンドル [ 2 ];

hイベント [ 0 ] = _hStopEvent ; // 停止イベントの方が優先されます。 彼を第一に考えましょう。

hイベント [ 1 ] = m_eventHasQuota ;

整数 iWaitResult = :: 複数のオブジェクトを待つ ( 2 , hイベント , 間違い , 無限 );

スイッチ( iWaitResult ) {

場合 WAIT_FAILED :

// エラー

新しいものを投げる CWin32例外 ;

場合 WAIT_OBJECT_0 :

// イベントを停止します。 私は特別な例外を使ってこれを処理しましたが、他の方法で実装することを妨げるものは何もありません。

新しいものを投げる CStopException ;

場合 WAIT_OBJECT_0 + 1 :

// イベント「割り当て可能」

アサート ( m_nクォータ ); // このイベントがシグナルを発していても、実際には割り当てがない場合は、どこかで間違いがあったことになります。 バグを探す必要があります!

もし( _nリクエスト >= m_nクォータ ) {

n提供する = m_nクォータ ;

m_nクォータ = 0 ;

m_eventNoQuota . セット ();

それ以外 {

n提供する = _nリクエスト ;

m_nクォータ -= _nリクエスト ;

m_eventHasQuota . セット ();

壊す;

戻る n提供する ;

ちょっとしたメモ。 MFC ライブラリはそのプロジェクトでは使用されませんでしたが、おそらくすでにお察しのとおり、私は MFC に似たカーネル イベント オブジェクトのラッパーである独自の CEvent クラスを作成しました。前述したように、このような単純なラッパー クラスは、次の場合に非常に役立ちます。作業の最後に忘れずに解放する必要がある特定のリソース (この場合はカーネル オブジェクト) がある場合は、SetEvent(m_hEvent) を書くか m_event.Set() を書くかに違いはありません。

この例が、重大な状況に遭遇した場合に独自の同期回路を設計するのに役立つことを願っています。 重要なことは、スキームをできるだけ注意深く分析することです。 正しく動作しない状況、特に詰まりが発生する可能性はありますか? デバッガーでこのようなエラーを検出するのは通常、絶望的な作業です。ここでは詳細な分析のみが役に立ちます。

ここまでは、最も重要なスレッド同期ツールであるカーネル同期オブジェクトについて見てきました。 これは強力で多用途なツールです。 これを利用すると、非常に複雑な同期スキームも構築できます。 幸いなことに、このような重要な状況はめったに発生しません。 さらに、汎用性には常にパフォーマンスが犠牲になります。 したがって、多くの場合、クリティカル セクションやアトミック操作など、Windows で利用可能な他のスレッド同期機能を使用する価値があります。 それらはそれほど普遍的ではありませんが、シンプルで効果的です。 それらについては次のパートで説明します。