Table of Contents
マルチスレッドアプリケーションでのFireDACの利用
本ブログでは、Delphi / C++Builderをある程度使用したことのある方をを対象として、FireDACの基本的な利用方法を解説していきます。
今回のテーマ
- マルチスレッド環境でのFireDACの使用方法
- コネクションプーリングの概要
- コネクションプーリングを利用したアプリケーション作成の演習
前回、FireDACのパフォーマンスチューニングについて解説しました。FireDACのデータアクセス再入門としてのトピックはひととおり網羅したのですが、今回はさらに応用編となるマルチスレッド環境でのFireDACの使用方法について、「番外編」として説明していきます。単一スレッドではなくマルチスレッドで実行させることによって、パフォーマンスの向上が見込めますが、同時に注意しなければならない点もあります。このブログでは、マルチスレッドアプリケーションでFireDACを利用する演習を作成し、よく理解を深めることができます。
マルチスレッド環境でのFireDACの使用方法
マルチスレッドアプリケーションでFireDACを利用する場合、あるスレッドでトランザクションを開始したら、そのトランザクションが完了するまで、そのトランザクションとFireDACのオブジェクトを別のスレッドで使用することができません。つまり、スレッド間でFireDACオブジェクトの共有は極力避けるべきです。
これらのルールに違反すると
- 誤動作
- アクセス違反エラー
- その他のエラー(例えば、 SQL Server エラー “他のコマンドの結果のために、接続がビジー状態になっています。” 等)
といった事象が発生する可能性があります。
FireDACでは、以下の条件を満たしている場合は、スレッドセーフとなります。
- 同じスレッド内でTFDConnectionなどの接続オブジェクトと、それに関連付けられているすべてのオブジェクト(例えば、 TFDQuery、TFDTransaction など)を実行している
- スレッドを開始する前に FDManager をアクティブに設定している
上記のルールを守るためには、TFDConnectionやTFDQueryなどのFireDACオブジェクトを扱うための専用スレッドを作成し、使用する必要があります。つまり、データベースへの接続、SQL文などの処理を実行した後、その接続の解放といった一連のサイクルを、専用のスレッド内で全て完結させることが求められます。
ただ、このような実装を行ってしまうと、 毎回スレッドごとにデータベースへの接続を確立しなければなりません。データベースへの接続の確立という操作は・・・実は高コストで、オーバーヘッドが大きい動作の1つなのです。
データベースへの接続は、まずデータベースのクライアントドライバとの間にTCP接続の確立を行います。それに続いてユーザ認証の手続きを行う必要があります。それが完了した後、ようやくSQL文をデータベースに対して発行することができます。そしてデータベースへ接続したコネクションを解放する際も、同様にアプリケーションから見えない部分で複雑な手順が行われています。
つまりスレッドごとにデータベースへの接続と解放を繰り返していると、システム全体にわたってパフォーマンスを低下させる要因になりかねません。これを避けるには、アプリケーション側で「コネクションプーリング」という仕組みを利用します。
コネクションプーリングの概要
コネクションプーリングとは、データベースへのコネクションをあらかじめ一定数確立しておき、それを「使いまわす」手法のことです。
例えば、クライアントドライバがデータベースへ接続する場合、あらかじめ確立済みのコネクションを利用します。さらにクライアントドライバがコネクションを解放する場合、通常の方法であればそのまま解放されますが、コネクションプーリングでは解放せず、コネクションをプールして再利用できるようにします。
確立済みのコネクションを再利用することによって、データベースへの接続にかかるコストを削減し、アプリケーションとデータベース両方にかかる負荷を軽減するメリットが得られます。
マルチスレッドで動作することが前提の現在のWebシステムでは、コネクションプーリングはパフォーマンス向上の目的でごく一般的に実装されていますが、FireDACでも、このコネクションプーリングの機能は標準で利用できます。
FireDACでコネクションプーリングを有効にするためには、以下の設定が必要です。
(1) FDManagerを有効にする
FDManagerは、 接続定義と接続オブジェクトを管理しているシングルトンのグローバルオブジェクト変数で、明示的なインスタンスの作成は不要です。
FDManagerのActiveプロパティをTrue、あるいはOpenメソッドを実行することでFDManagerを有効にすることができます。
(2) Params.Pooled = Trueに設定する
永続接続定義または非公開接続定義されたの場合にのみ、Pooled=True に設定すること有効になります。
例えば、永続接続定義とは、RAD Studioの開発環境では、以下の定義ファイルに保持されている情報です。
1 |
C:\Users\Public\Documents\Embarcadero\Studio\FireDAC\FDConnectionDefs.ini |
IDEメニューの[ツール]-[FireDACエクスプローラ]で永続接続定義されているリストを見ることができます。
例えば、FDConnectionDefs.iniファイルをエディタで開くと、以下のように定義されています。
1 2 3 4 5 6 7 8 |
[EMPLOYEE] DriverID=IB Protocol=TCPIP Database=localhost:C:\Users\Public\Documents\Embarcadero\Studio\21.0\Samples\data\employee.gdb User_Name=sysdba Password=masterkey CharacterSet= ExtendedMetadata=True |
それでは、実際にコネクションプーリングを利用したアプリケーション作成の演習を行い、実装方法について詳しく見てきたいと思います。
コネクションプーリングを利用したアプリケーション作成の演習
このセクションでは、コネクションプーリングを利用したアプリケーション作成の演習を行います。
今回のブログでも、接続するデータベースとしてInterBase 2020を使用します。
そのため事前にInterBaseのプロセスを起動しておいてください。過去のシリーズの演習でInterBaseの起動手順も解説していますので、詳しくはこちらをご覧ください。
演習手順は以下の通りです。
- データベースへアクセスするためのスレッドクラスを定義
- スレッドクラスのExecuteメソッドを実装
- フォームからFireDACへアクセスするスレッドを実行
- コネクションプーリングの利用有無で実行速度の違いを確認
(1) VCLフォームアプリケーションのプロジェクト作成
Delphi / C++Builderのメニューから[ファイル]-[新規作成]-[Windows VCLフォームアプリケーション]を選択します。
(2) プロジェクトを保存する
メニューの[ファイル]-[すべて保存] を選択し、全てのファイルを保存してください。プロジェクトは、任意のフォルダに保存することができます。
(3) フォーム上にFireDACのコンポーネントを配置する
ツールパレットの[FireDAC]カテゴリから
・TFDConnection
・TFDQuery
ツールパレットの[FireDAC Links]カテゴリから
・TFDPhysIBDriverLink
(4) フォーム上にUIコンポーネントを配置する
ツールパレットの[Standard]カテゴリから
・TLabel
を4つ
・TCheckBox
を1つ
・TButton
を1つ
フォーム上の任意の位置へ配置します(下図のは配置例を参照)
(5) 配置したコントロールのプロパティを変更する
Label1
プロパティ | 値 |
---|---|
Text | 実行回数: |
Label2
プロパティ | 値 |
---|---|
Text | 実行時間: |
Label3
プロパティ | 値 |
---|---|
Text | — |
Label4
プロパティ | 値 |
---|---|
Text | — |
CheckBox1
プロパティ | 値 |
---|---|
Caption | コネクションプーリング |
Button1
プロパティ | 値 |
---|---|
Caption | 実行 |
各プロパティの値を変更後の画面イメージは、下図の通りです。
FireDACのコンポーネントを配置していますが、プロパティの変更は必要ありません。 これらのコンポーネントを配置している目的は、プロジェクトをビルドしたときにFireDAC関連のユニットがソースコードへ自動的にインクルード、あるいはuses句に追加されるためです。 |
(5) データベースへアクセスするためのスレッドクラスを定義
TThreadクラスから派生するFireDACオブジェクトヘアクセスするためのスレッドクラスを定義します。ここでは、TDBThreadクラスという名前で定義します。
Delphi:
Unit1.pasを開いて、interface句に以下のコードを追加してください。
1 2 3 4 5 6 7 8 |
type TDBThread = class(TThread) private FForm: TForm1; public constructor Create(AForm: TForm1); procedure Execute; override; end; |
C++Builder:
Unit1.hを開いて、以下のコードを追加してください。
1 2 3 4 5 6 7 8 |
class TDBThread : public TThread { private: TForm1 *FForm; public: __fastcall TDBThread(TForm1 *AForm); void __fastcall Execute(); }; |
(6) Form1クラスにメソッドと変数を定義
Form1クラスに実行の開始時間や実行回数の値を保持するメンバー変数と、これらを表示するためにメソッドを定義します。
Delphi:
Unit1.pasを開いて、Form1クラスに以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 |
type TForm1 = class(TForm) .. .. private { Private 宣言 } FCount: Integer; FStartTime: LongWord; public { Public 宣言 } procedure Executed; end; |
C++Builder:
Unit1.hを開いて、Form1クラスに以下のコードを追加してください
1 2 3 4 5 6 7 8 9 10 11 12 |
class TForm1 : public TForm { .. .. private: // ユーザー宣言 int FCount; unsigned long FStartTime; public: // ユーザー宣言 __fastcall TForm1(TComponent* Owner); void __fastcall Executed(); }; |
(6) Form1クラスのExecutedメソッドを実装
手順(5)で定義したForm1のExecutedメソッドの実装コードを追加します。
Delphi:
Unit1.pasを開いて、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
procedure TForm1.Executed; begin Inc(FCount); // 10回ごとにLabel3に回数を表示する if (FCount mod 10) = 0 then Label3.Caption := IntToStr(FCount); // 実行回数が500に達したら、実行時間を表示する if FCount = 500 then begin // 表示される実行時間は、 ms単位 Label4.Caption := FloatToStr((GetTickCount - FStartTime) / 1000.0); Button1.Enabled := True; end; end; |
C++Builder:
Unit1.cppを開いて、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void __fastcall TForm1::Executed() { FCount++; // 10回ごとにLabel3に回数を表示する if ((FCount % 10) == 0) { Label3->Caption = IntToStr(FCount); } // 実行回数が500に達したら、実行時間を表示する if (FCount == 500) { // 表示される実行時間は、 ms単位 Label4->Caption = FloatToStr((GetTickCount() - FStartTime) / 1000.0); Button1->Enabled = True; } } |
(7) スレッドクラスのコンストラクタを実装
スレッドクラスのコンストラクタのコードを実装します。
Delphi:
Unit1.pasを開いて、以下のコードを追加してください。
1 2 3 4 5 6 |
constructor TDBThread.Create(AForm: TForm1); begin FForm := AForm; FreeOnTerminate := True; inherited Create(False); end; |
C++Builder:
Unit1.cppを開いて、以下のコードを追加してください。
1 2 3 4 |
__fastcall TDBThread::TDBThread(TForm1 *AForm) : TThread(false) { FForm = AForm; FreeOnTerminate = true; } |
FreeOnTerminateは、スレッド終了時にスレッドオブジェクトを自動的に破棄するかどうかを決定します。FreeOnTerminate に True のときは、自動的に破棄されません。今回のコードでは、スレッドが終了しても破棄せず保持する必要があるためTrueに設定しています。 |
(8) スレッドクラスのExecuteメソッドを実装
スレッドクラスのExecuteメソッドのコードを実装します。本スレッドは、FireDACのオブジェクトへアクセスすることが目的のため、
TFDConnectionによるデータベースへの接続と解放をこのスレッド内で行っています。
Delphi:
Unit1.pasを開いて、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
procedure TDBThread.Execute; var oConn: TFDConnection; oQuery: TFDQuery; i: Integer; begin oConn := TFDConnection.Create(nil); oQuery := TFDQuery.Create(nil); try oConn.ConnectionDefName := 'EMPLOYEE'; oQuery.Connection := oConn; for i := 1 to 50 do begin oQuery.SQL.Text := 'select * from Employee'; oQuery.Open; oConn.Close; Synchronize(FForm.Executed); //VCLの描画スレッドへ同期 end; finally oConn.Free; oQuery.Free; end; end; |
C++Builder:
Unit1.cppを開いて、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void __fastcall TDBThread::Execute() { TFDConnection *oConn=new TFDConnection(NULL); TFDQuery *oQuery=new TFDQuery(NULL); try { oConn->ConnectionDefName = "EMPLOYEE"; oQuery->Connection = oConn; for (int i = 0; i < 50; i++) { oQuery->SQL->Text = "select * from Employee"; oQuery->Open(); oConn->Close(); Synchronize(FForm->Executed); //VCLの描画スレッドへ同期 } } __finally { delete oConn; delete oQuery; } } |
FDConnection.ConnectionDefNameプロパティには、定義済みの接続定義を設定する必要があります。ConnectionDefNameに渡している”EMPLOYEE”は、FDConnectionDefs.iniにあらかじめ定義されている接続定義です。もしFDConnectionDefs.iniに保持されている情報に該当しない接続定義の文字列を指定した場合は、エラーになります。 |
(9) 実行ボタンのコードを実装
設計画面でButton1をダブルクリックすると、Button1のOnClickのイベントハンドラが生成されます。そのイベントハンドラ内に、スレッドを実行する処理などを実装します。
Delphi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
procedure TForm1.Button1Click(Sender: TObject); var i: Integer; begin Button1.Enabled := False; FDManager.Close; FDManager.Open; //チェックがonのときは、コネクションプーリングを有効にする if CheckBox1.Checked then FDManager.ConnectionDefs.ConnectionDefByName('EMPLOYEE').Params.Pooled := True else FDManager.ConnectionDefs.ConnectionDefByName('EMPLOYEE').Params.Pooled := False; FStartTime := GetTickCount; FCount := 0; Label3.Caption := '---'; Label4.Caption := '---'; for i := 1 to 10 do begin // スレッドの生成 TDBThread.Create(Self); end; end; |
C++Builder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void __fastcall TForm1::Button1Click(TObject *Sender) { Button1->Enabled = false; FDManager()->Close(); FDManager()->Open(); //チェックがonのときは、コネクションプーリングを有効にする if (CheckBox1->Checked) { FDManager()->ConnectionDefs->ConnectionDefByName("EMPLOYEE")->Params->Pooled = true; } else { FDManager()->ConnectionDefs->ConnectionDefByName("EMPLOYEE")->Params->Pooled = false; } FStartTime = GetTickCount(); FCount = 0; Label3->Caption = "---"; Label4->Caption = "---"; for (int i = 0; i < 10; i++) { // スレッドの生成 new TDBThread(this); } } |
コネクションプーリングを有効にするためには、FDConnection.Params.Pooled=trueに設定します。なお、FDManagerが参照している接続定義はConnectionDefsで見ることができます。ConnectionDefs.ConnectionDefByName(‘EMPLOYEE’).Params.Pooled = trueに設定することで、”EMPLOYEE”の接続定義に対してコネクションプーリングを有効にすることができます。 |
(10) プロジェクトを保存する
メニューの[ファイル]-[すべて保存] を選択し、全てのファイルを保存してください。
(11) アプリケーションを実行する
ツールバー(上図)の実行ボタン、または、キーボードの[F9]ボタンを押します。
(12) [実行]ボタンを押す
[実行]ボタンを押すと、スレッドが500回実行され、実行時間が表示されます。
(13) コネクションプーリングにチェックを入れて、[実行]ボタンを押す
今度は、コネクションプーリングにチェックを入れて、[実行]ボタンを押してください。
同様にスレッドが500回実行され、実行時間(ms)が表示されます。
どうでしょうか?
実行結果の両方を比較すると、コネクションプーリングを有効にすると同じ実行回数でも実行時間(ms)が、より速いことが実感できたと思います。
今回はあくまでサンプルブログラムなので、スレッドの実行回数も少なく、ローカル実行しているInterBaseへアクセスしているということもあって、接続への確立に対するオーバーヘッドも少なく両方の実行時間の差は、あまり感じられないかもしれませんが、実際のシステムでは、実行回数はこれよりも多くなり、リモート上のデータベースへのアクセスすることになりますので、その差は顕著になります。
まとめ
現在のアプリケーションは、マルチスレッド環境で実行していることが多く、特にデータベースへアクセスする必要がある場合、パフォーマンスを維持する目的としてコネクションプーリングという仕組みが必須となります。
FireDACでは、標準でコネクションプーリングが利用できるため、今回のサンプルプログラムでご覧いただいたクライアントサーバー型の2層アプリケーション以外にも、3層/多層型のサーバーアプリケーションでFireDACを使用してデータアクセス機能を実装する際にも、同様のパフォーマンス最適化手段として用いることができるのです。
実際、Delphi / C++Builder / (RAD Studio)によって中間サーバー(REST API)を構築できる「RAD Server」では、そのデータアクセスレイヤーに関する重要なインフラとしてコネクションプーリングを活用したFireDACの技術が用いられています。
FireDACは、クライアントサーバー型アプリケーションのクライアント側のデータアクセス、デスクトップアプリケーションやモバイルアプリのローカルデータアクセスとして利用されるだけでなく、多層アプリケーションの中間サーバーやクラウド環境におけるデータアクセスエンジンとしても利用できるのです。
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition