Delphiのスレッドクラス
ラプター[メンタルスタジオ]
http://mental.mentsu.com
4
クリティカル セクション (CriticalSection) は、共有データ アクセス保護のためのテクノロジです。実際には、グローバル ブール変数と同等です。ただし、その操作は Enter と Leave の 2 つだけであり、その 2 つの状態はそれぞれ True と False とみなされ、クリティカル セクションにあるかどうかを示します。これら 2 つの操作も基本的なため、マルチスレッド アプリケーションで共有データをアクセス違反から保護するために使用できます。
クリティカル セクションを使用して共有データを保護する方法は非常に簡単です。毎回共有データにアクセスする前に Enter を呼び出してクリティカル セクション フラグを設定し、データを操作して、最後に Leave を呼び出してクリティカル セクションから離れます。その保護原理は次のとおりです。スレッドがクリティカル セクションに入った後、この時点で別のスレッドもデータにアクセスしたい場合、Enter を呼び出したときにスレッドがすでにクリティカル セクションに入っていることがわかり、このスレッドは電話を切り、現在クリティカル セクションにあるスレッドが Leave を呼び出してクリティカル セクションから離れるまで待ちます。別のスレッドが操作を完了し、Leave を呼び出してクリティカル セクションから離れると、このスレッドが起動され、クリティカル セクション フラグが設定され、データの操作が開始されます。したがって、アクセスの競合を防ぎます。
前の InterlockedIncrement を例として、CriticalSection (Windows API) を使用して実装します。
ヴァール
InterlockedCrit : TRTLCriticalSection;
PROcedure InterlockedIncrement( var aValue : Integer );
始める
EnterCriticalSection(InterlockedCrit);
Inc(aValue);
LeaveCriticalSection(InterlockedCrit);
終わり;
次に、前の例を見てみましょう。
1. スレッド A がクリティカル セクションに入る (データが 3 であると仮定)
2. スレッド B がクリティカル セクションに入ります。A はすでにクリティカル セクションに入っているため、B は一時停止されます。
3. スレッド A がデータに 1 を追加します (現在は 4)。
4. スレッド A がクリティカル セクションを離れ、スレッド B を起動します (メモリ内のデータは現在 4 です)
5. スレッド B が起動し、データに 1 を追加します (現在は 5 です)。
6. スレッド B はクリティカル セクションを離れ、現在のデータは正しいです。
これは、重要なセクションが共有データへのアクセスを保護する方法です。
クリティカル セクションの使用に関して注意すべき点が 1 つあります。それは、データ アクセス時の例外の処理です。データ操作中に例外が発生した場合、Leave 操作が実行されず、その結果、起動すべきスレッドが起動されず、プログラムが応答しなくなる可能性があります。したがって、一般的に言えば、正しいアプローチは次のようにクリティカル セクションを使用することです。
クリティカルセクションに入る
試す
//クリティカルセクションデータの操作
ついに
クリティカルセクションを離れる
終わり;
最後に注意すべきことは、Event と CriticalSection はどちらもオペレーティング システム リソースであり、使用前に作成し、使用後に解放する必要があるということです。たとえば、TThread クラスによって使用されるグローバル Event: SyncEvent とグローバル CriticalSection: TheadLock は、どちらも InitThreadSynchronization と DoneThreadSynchronization で作成および解放され、クラス ユニットの初期化と終了で呼び出されます。
TThread では Event と CriticalSection を操作するために API が使用されるため、実際には、Delphi はそれらのカプセル化を SyncObjs ユニットで提供しています。これらはそれぞれ TEvent クラスと TCriticalSection クラスです。使い方は先ほどのAPIの使い方とほぼ同じです。 TEvent のコンストラクターにはパラメーターが多すぎるため、簡単にするために、Delphi はデフォルトのパラメーターで初期化された Event クラス TSimpleEvent も提供します。
ところで、スレッド同期に使用される別のクラスである TMultiReadExclusiveWriteSynchronizer を紹介します。これは SysUtils ユニットで定義されています。私の知る限り、これは Delphi RTL で定義されている最も長いクラス名です。幸いなことに、TMREWSync という短いエイリアスが付いています。用途については、名前を見れば分かると思いますので、これ以上は言いません。
Event と CriticalSection に関するこれまでの準備知識があれば、Synchronize と WaitFor について正式に説明し始めることができます。
プロセス内にはメイン スレッドが 1 つしかないため、Synchronize はコードの一部をメイン スレッドに配置して実行することによってスレッド同期を実現することがわかっています。まず、Synchronize の実装を見てみましょう。
プロシージャ TThread.Synchronize(メソッド: TThreadMethod);
始める
FSynchronize.FThread := Self;
FSynchronize.FSynchronizeException := nil;
FSynchronize.FMethod := メソッド;
同期(@FSynchronize);
終わり;
ここで、FSynchronize はレコード タイプです。
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = レコード
Fスレッド: Tオブジェクト;
Fメソッド: TThreadメソッド;
FSynchronizeException: TObject;
終わり;
スレッドとメインスレッド間のデータ交換に使用されます。これには、受信スレッドクラスオブジェクト、同期メソッド、発生する例外が含まれます。
そのオーバーロードされたバージョンは Synchronize で呼び出されます。このオーバーロードされたバージョンは非常に特殊で、「クラス メソッド」です。いわゆるクラス メソッドは特別なクラス メンバー メソッドであり、その呼び出しにはクラス インスタンスの作成は必要ありませんが、コンストラクターのようにクラス名を通じて呼び出されます。クラスメソッドで実装しているのは、スレッドオブジェクトを作成していなくても呼び出せるようにするためです。ただし、実際には、別のオーバーロードされたバージョン (これもクラス メソッド) と別のクラス メソッド StaticSynchronize が使用されます。この同期のコードは次のとおりです。
クラス プロシージャ TThread.Synchronize(ASyncRec: PSynchronizeRecord);
変数
SyncProc: TSyncProc;
始める
GetCurrentThreadID = MainThreadID の場合、
ASyncRec.Fメソッド
それ以外
始める
SyncProc.Signal := CreateEvent(nil, True, False, nil);
試す
EnterCriticalSection(ThreadLock);
試す
SyncList = nil の場合
同期リスト := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
信号同期イベント;
割り当て済み(WakeMainThread)の場合
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection(ThreadLock);
試す
WaitForSingleObject(SyncProc.Signal, INFINITE);
ついに
EnterCriticalSection(ThreadLock);
終わり;
ついに
LeaveCriticalSection(ThreadLock);
終わり;
ついに
CloseHandle(SyncProc.Signal);
終わり;
Assigned(ASyncRec.FSynchronizeException) の場合、ASyncRec.FSynchronizeException が発生します。
終わり;
終わり;
このコードは少し長くなりますが、それほど複雑ではありません。
1 つ目は、現在のスレッドがメイン スレッドであるかどうかを判断することです。メイン スレッドである場合は、単純に同期メソッドを実行して戻ります。
メインスレッドでない場合は、同期プロセスを開始する準備ができています。
スレッド交換データ (パラメーター) とイベント ハンドルは、ローカル変数 SyncProc を通じて記録されます。レコード構造は次のとおりです。
TSyncProc=レコード
SyncRec: PSynchronizeRecord;
信号: THandle;
終わり;
次に、イベントを作成し、(グローバル変数 ThreadLock を介して) クリティカル セクションに入ります。同期状態に同時に入ることができるのは 1 つのスレッドだけであるため、グローバル変数を使用して記録できます)、記録されたデータをSyncList リスト (リストが存在しない場合は作成します)。 ThreadLock の重要なセクションは SyncList へのアクセスを保護することであることがわかります。これは、後で CheckSynchronize が導入されるときに再び表示されます。
次のステップは、SignalSyncEvent を呼び出すことです。そのコードは、以前に TThread コンストラクターを導入したときに導入されました。その機能は、単に SyncEvent に対して Set 操作を実行することです。この SyncEvent の目的については、後で WaitFor を導入するときに詳しく説明します。
次は最も重要な部分です。同期操作のために WakeMainThread イベントを呼び出します。 WakeMainThread は、TNotifyEvent タイプのグローバル イベントです。ここでの処理にイベントが使用される理由は、Synchronize メソッドは基本的に、同期する必要があるプロセスをメッセージを通じて実行するメイン スレッドに置くためです。メッセージ ループのない一部のアプリケーション (コンソールや DLL など) では使用できません。なので、このイベントを処理に使用します。
アプリケーション オブジェクトは、このイベントに応答します。次の 2 つのメソッドを使用して、(Forms ユニットからの) WakeMainThread イベントへの応答を設定およびクリアします。
プロシージャ TApplication.HookSynchronizeWakeup;
始める
Classes.WakeMainThread := WakeMainThread;
終わり;
プロシージャ TApplication.UnhookSynchronizeWakeup;
始める
Classes.WakeMainThread := nil;
終わり;
上記 2 つのメソッドは、それぞれ TApplication クラスのコンストラクターとデストラクターで呼び出されます。
これは、Application オブジェクトの WakeMainThread イベントに応答するコードであり、これを実現するために空のメッセージが使用されます。
プロシージャ TApplication.WakeMainThread(送信者: TObject);
始める
PostMessage(ハンドル, WM_NULL, 0, 0);
終わり;
このメッセージに対する応答も Application オブジェクト内にあります。次のコードを参照してください (無関係な部分を削除します)。
プロシージャ TApplication.WndProc(var メッセージ: TMessage);
…
始める
試す
…
メッセージ付き
のケースメッセージ
…
WM_NULL:
チェック同期;
…
を除外する
HandleException(自己);
終わり;
終わり;
このうち、CheckSynchronize も Classes ユニット内で定義されているため、ここでは特に Synchronize 関数を処理する部分であることを理解してください。コード。
WakeMainThread イベントを実行した後、クリティカル セクションを終了し、WaitForSingleObject を呼び出して、クリティカル セクションに入る前に作成されるイベントの待機を開始します。このイベントの機能は、この同期メソッドの実行が終了するのを待つことです。これについては、CheckSynchronize を分析するときに説明します。
WaitForSingleObject の後、クリティカル セクションに再度入りますが、何もせずに終了することに注意してください。これは無意味に見えますが、必要です。
クリティカルセクションの Enter と Leave は厳密に 1 対 1 に対応する必要があるためです。したがって、これを次のように変更できますか?
割り当て済み(WakeMainThread)の場合
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
ついに
LeaveCriticalSection(ThreadLock);
終わり;
上記のコードと元のコードの最大の違いは、WaitForSingleObject もクリティカル セクションの制限に含まれていることです。影響はないようで、コードが大幅に簡素化されますが、本当に可能でしょうか?
実際、そうではありません。
クリティカル セクションに入る後、他のスレッドが再度入りたい場合は中断されることがわかっているためです。 WaitFor メソッドは現在のスレッドを一時停止し、他のスレッドの SetEvent を待つまで起動されません。コードを上記のように変更した場合、SetEvent スレッドもクリティカル セクションに入る必要がある場合、デッドロックが発生します (デッドロックの理論については、オペレーティング システムの原理に関する情報を参照してください)。
デッドロックはスレッド同期の最も重要な側面の 1 つです。
最後に、最初に作成したイベントを解放します。同期されたメソッドが例外を返した場合、ここで再度例外がスローされます。
(つづく)