Delphiのスレッドクラス
ラプター[メンタルスタジオ]
http://mental.mentsu.com
パート 2
1 つ目はコンストラクターです。
コンストラクター TThread.Create(CreateSuspended: Boolean);
始める
継承された作成。
スレッドを追加します。
FSuspended := CreateSuspended;
FCreateSuspended := CreateSuspended;
FHandle := BeginThread(nil, 0, @ThreadPRoc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
FHandle = 0 の場合
raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]);
終わり;
このコンストラクターにはそれほど多くのコードはありませんが、ここでスレッドが作成されるため、最も重要なメンバーとみなすことができます。
Inherited を通じて TObject.Create を呼び出した後の最初の文は、プロセス AddThread を呼び出すことであり、そのソース コードは次のとおりです。
プロシージャAddThread;
始める
InterlockedIncrement(ThreadCount);
終わり;
対応する RemoveThread もあります。
プロシージャ RemoveThread;
始める
InterlockedDecrement(ThreadCount);
終わり;
それらの機能は非常に単純で、グローバル変数を増減することによってプロセス内のスレッドの数をカウントすることです。ここでは、一般的に使用される Inc/Dec プロセスは変数の増減に使用されませんが、InterlockedIncrement/InterlockedDecrement プロセスのペアがまったく同じ関数を実装し、変数に 1 を加算または減算します。ただし、大きな違いが 1 つあります。それは、InterlockedIncrement/InterlockedDecrement はスレッドセーフであるということです。つまり、マルチスレッド下で正しい実行結果を保証できますが、Inc/Dec は保証できません。または、オペレーティング システム理論の用語で言えば、これは 1 対の「原始的な」操作です。
プラス 1 を例として、2 つの実装の詳細の違いを説明します。
一般に、メモリ データに 1 を加算する操作には、分解後に 3 つのステップがあります。
1. メモリからデータを読み取る
2. データプラスワン
3. メモリに保存する
次に、Inc を使用して 2 スレッド アプリケーションでインクリメント操作を実行するときに発生する可能性のある状況を想定します。
1. スレッド A がメモリ (3 と仮定) からデータを読み取ります。
2. スレッド B がメモリからデータを読み取ります (3 も同様)
3. スレッド A がデータに 1 を追加します (現在は 4 です)。
4. スレッド B はデータに 1 を追加します (これも 4 になります)。
5. スレッド A がデータをメモリに保存します (メモリ内のデータは現在 4 です)
6. スレッド B もデータをメモリに保存します (メモリ内のデータは 4 のままですが、両方のスレッドがそれに 1 を加算しており、本来は 5 であるため、ここでは誤った結果が生じています)。
InterlockIncrement プロセスではそのような問題はありません。いわゆる「プリミティブ」は中断不可能な操作であるためです。つまり、オペレーティング システムは、「プリミティブ」が実行される前にスレッドの切り替えが発生しないことを保証できます。したがって、上記の例では、スレッド A が実行とデータのメモリへの保存を完了した後でのみ、スレッド B が数値のフェッチと 1 の追加を開始できるため、マルチスレッド状況でも結果は同じになります。正しい。
前の例では、「スレッド アクセスの競合」状況も示しています。そのため、スレッドを「同期」する必要があります (同期)。これについては、後で同期について説明するときに詳しく説明します。
同期と言えば余談ですが、カナダのウォータールー大学のリー・ミン教授はかつて、Synchronize という言葉が「スレッド同期」の「同期」と訳されることに異議を唱えましたが、個人的には彼の言ったことは実際その通りだと思います。とても合理的です。中国語で「同期」は「同時に起こる」という意味で、「スレッド同期」の目的はこの「同時に起こる」ことを避けることです。英語の Synchronize には 2 つの意味があります。1 つは伝統的な意味での同期(同時に起こること)、もう 1 つは「一斉に動作する」(一斉に動作する)です。 「スレッド同期」の「同期」という言葉は後者の意味、つまり「複数のスレッドが調整を維持し、同じデータにアクセスする際のエラーを確実に回避すること」を指す必要があります。ただし、IT 業界ではこのような不正確な翻訳がまだ多くあり、ソフトウェア開発は慎重な作業であるため、この記事では引き続き説明します。決してそうであってはなりません 曖昧であってはなりません。
さらに一歩進んで、TThread のコンストラクターに戻ります。次に重要なのは、次の文です。
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
ここでは、前述した Delphi RTL 関数 BeginThread が使用されており、多くのパラメーターがあり、重要なのは 3 番目と 4 番目のパラメーターです。 3 番目のパラメーターは、前述のスレッド関数、つまりスレッドで実行されるコードの一部です。 4 番目のパラメータはスレッド関数に渡されるパラメータで、ここでは作成されたスレッド オブジェクト (つまり Self) です。他のパラメーターのうち、5 番目はスレッドを作成後に一時停止し、すぐには実行しないように設定するために使用されます (スレッドの開始作業は AfterConstruction の CreateSuspended フラグに従って決定されます)。6 番目はスレッド ID を返すために使用されます。
次に、TThread のコアであるスレッド関数 ThreadProc を見てみましょう。興味深いのは、このスレッド クラスのコアがスレッドのメンバーではなく、グローバル関数であることです (BeginThread プロセスのパラメーター規約ではグローバル関数のみを使用できるため)。そのコードは次のとおりです。
関数 ThreadProc(スレッド: TThread): 整数;
変数
フリースレッド: ブール値。
始める
試す
Thread.Terminated ではない場合
試す
スレッド.実行;
を除外する
Thread.FFatalException := AcquireExceptionObject;
終わり;
ついに
FreeThread := Thread.FFreeOnTerminate;
結果 := Thread.FReturnValue;
Thread.DoTerminate;
Thread.FFinished := True;
信号同期イベント;
FreeThread の場合は Thread.Free;
EndThread(結果);
終わり;
終わり;
コードはそれほど多くありませんが、このコードはスレッド内で実際に実行されるコードであるため、TThread 全体の中で最も重要な部分です。コードを 1 行ずつ説明すると、次のようになります。
まず、スレッド クラスの Terminated フラグを確認します。終了済みとしてマークされていない場合は、スレッド クラスの Execute メソッドを呼び出して、スレッド コードを実行します。これは、TThread が抽象クラスであり、Execute メソッドが抽象メソッドであるためです。派生クラスで実行コードを実行します。
したがって、Execute はスレッド クラスのスレッド関数であり、アクセスの競合を防ぐなど、Execute 内のすべてのコードはスレッド コードとして考慮される必要があります。
Execute で例外が発生した場合、例外オブジェクトは AcquireExceptionObject を通じて取得され、スレッド クラスの FFatalException メンバーに格納されます。
最後に、スレッドが終了する前にいくつかの仕上げがあります。ローカル変数 FreeThread は、スレッド クラスの FreeOnTerminated 属性の設定を記録し、スレッドの戻り値をスレッド クラスの戻り値属性の値に設定します。次にスレッドクラスのDoTerminateメソッドを実行します。
DoTerminate メソッドのコードは次のとおりです。
プロシージャ TThread.DoTerminate;
始める
Assigned(FOnTerminate)の場合は、Synchronize(CallOnTerminate);
終わり;
これは非常に簡単で、Synchronize を介して CallOnTerminate メソッドを呼び出すだけです。CallOnTerminate メソッドのコードは次のとおりであり、単に OnTerminate イベントを呼び出すだけです。
プロシージャ TThread.CallOnTerminate;
始める
Assigned(FOnTerminate)の場合はFOnTerminate(Self);
終わり;
OnTerminate イベントは Synchronize で実行されるため、本質的にはスレッド コードではなく、メイン スレッド コードです (詳細については、後述の Synchronize の分析を参照してください)。
OnTerminate を実行した後、スレッド クラスの FFinished フラグを True に設定します。
次に、SignalSyncEvent プロセスが実行されます。そのコードは次のとおりです。
プロシージャ SignSyncEvent;
始める
SetEvent(同期イベント);
終わり;
これも非常に簡単で、グローバル イベント SyncEvent を設定するだけです。この記事では Event の使用方法について詳しく説明し、SyncEvent の目的については WaitFor プロセスで説明します。
次に、FreeThread に保存されている FreeOnTerminate 設定に基づいてスレッド クラスを解放するかどうかが決定されます。詳細については、次のデストラクターの実装を参照してください。
最後に、EndThread を呼び出してスレッドを終了し、スレッドの戻り値を返します。
この時点で、スレッドは完全に終了します。
(つづく)