Delphi Tips - マルチスレッドアプリケーション

概要: Delphi Tips - マルチスレッドアプリケーション

Delphi 2.0J/3.1Jには、Win32でサポートされているマルチスレッドアプリケーションを簡単に作成することのできる、TThreadオブジェクトが用意されています。ここでは、Delphi 2.0J/3.1Jでのマルチスレッドの使い方を説明します。

    1. TThread

    スレッドとは

プログラムは、メモリ上にロードされ、ひとつの「プロセス」として実行されます。プロセスは、ひとつのスレッドから起動しますが、ひとつのプロセスから、複数のスレッドを生成することもできます。ひとつのプロセスは、ひとつの仮想アドレス空間を持ちます。

「スレッド」とは、プログラムの一部の実行単位です。スレッドは、 オペレーティングシステムがCPU時間を割り当てるときの基本単位です。ひとつのプロセスのすべてのスレッドは、同じ仮想アドレス空間を共有します。

Windows95/NTは、CPU時間を必要とする各スレッド間で、CPU時間を少しずつ割り当てます。現在実行されているスレッドは、20ミリ秒のオーダーの「タイムスライス」が経過すると中断され、次のスレッドが動作します。複数のスレッドが、並列処理されているように見えるのはこのためです。

マルチスレッドが役に立つのは、複数の作業を同時に実行する場合です。例えば、異なるサーバにアクセスして、複数のデータセットを取得したい場合や、複数のウィンドウに対して、同時に描画を行ないたい場合などです。

マルチスレッドアプリケーションは、その利点と相反して、欠点や危険性を持っています。複数のスレッドを生成すれば、スレッドの管理に必要なメモリを消費し、 CPU時間が消費されます。複数のスレッドが同じリソースをアクセスする場合、衝突を防ぐためにスレッド間で同期をとらなくてはなりません。

    TThreadオブジェクト

Delphi 2.0J/3.1Jには、マルチスレッドアプリケーションを実現するために、TThreadオブジェクトを用意しています。TThreadオブジェクトは、スレッドを生成し、同期をとってコードを実行できる手続きを提供します。TThreadを用いれば、Win32APIで複雑なコードを記述することなく、マルチスレッドアプリケーションを作成できます。

    TThreadの使い方

TThreadは、必ず派生クラスを作成して使用します。TThreadには、Executeメソッドがあります。スレッドが生成されると、Executeメソッドが実行されます。このメソッドをオーバライドして、スレッドが実行するコードを記述します。

TThreadの派生クラスのコードの雛形は、オブジェクトリポジトリのスレッドオブジェクトにあります。[ファイル(F)|新規作成(N)] で、「スレッドオブジェクト」を選択すると、以下のようなユニットが生成されます。

   unit Unit2;
   
   interface
   
   uses
     Classes;
   
   type
     MyThread = class(TThread)
     private
       { Private 宣言 }
     protected
       procedure Execute; override;
     end;
   
   implementation
   
   {注意:省略}
   
   
   { MyThread }
   
   procedure MyThread.Execute;
   begin
     { スレッドとして実行したいコードをここに記述 }
   end;
   
   end.

スレッドが実行するコードは、Executeメソッドに記述します。繰り返し処理を行なう場合、Terminatedプロパティをチェックしながらループします。

   procedure MyThread.Execute;
   begin
     while not Terminated do begin
      :
      :
     end;
   end;

多くの処理は、VCLコンポーネントを使用します。しかし、通常VCLコンポーネントが使えるのは、メインスレッドからだけなので、衝突を避けるために、スレッドオブジェクトからはSynchronizeメソッドを使って、VCLコンポーネントにアクセスするメソッドを呼び出します。次の例は、ファイルからMemoに文字列を追加するスレッドです。

   procedure MyThread.ReadLine
   var
     buf : String;
   begin
     Readln(F, buf);
     FMemo.Lines.Add(buf);
   end;
   
   procedure MyThread.Execute;
   begin
     AssignFile(F, FileName);
     Reset(F);
     while not Terminated and not Eof(F) do
       Synchronize(ReadLine);
     CloseFile(F);
   end;

TMemo型のFMemoや、TextFile型のF変数、文字列型のFileNameは、MyThreadのメンバです。この例では、これらの変数をコンストラクタで初期化したり、用意しなければなりません。MyThreadのコンストラクタは、以下のようになります。

   constructor MyThread.Create(aFileName: String; aMemo: TMemo);
   begin
     FMemo := aMemo;
     FileName := aFileName;
     inherited Create(False);
     FreeOnTerminate := True;
   end;

TThreadのコンストラクタのFalseという引数は、スレッドが直ちに実行されることを示します。FreeOnTerminateプロパティをTrueに設定することで、スレッドが終了したときにオブジェクトが開放されます。

    どのようなときに使うのか?

どのような場面で、スレッドオブジェクトを使う必要があるのでしょうか?マルチスレッドアプリケーションは、並行処理を行なうプログラムです。従って、「ある処理を行なっているときに、同時に別の処理を行ないたいとき」というのが答えになります。

例えば、ユーザーの入力処理中に、アニメーションを表示したいときや、大きなデータを読み込んでいる最中に、次の入力処理に応答させたい場合などです。また、クライアントサーバ環境では、マルチスレッドによって、複数のサーバへ同時にクエリーを発行することもできます。ただし、注意しなければならないのは、ローカルデータベースへのアクセスにおいては、マルチスレッドの使用は逆効果だということです。

    2. マルチスレッドの使用例(1)

    アニメーション

マルチスレッドを用いてアニメーションを行なうには、一般的に複数のイメージを順番に切り替えて実現します。映画のコマ送りのようなビットマップをたくさん用意して、順番に切り替えます。ここでは、ビットマップがなくても簡単に確認できる例として、パネル上に表示する文字列を順次切り替えてみます。

サンプルフォームは、簡単な設計です。Panel、Memo、Buttonを貼り付けたシンプルなフォームです。Panelのフォントは、見栄えがいいように変更しておきます。

Hide image
fig_1

図1

    スレッドオブジェクトの作成

Threadオブジェクトは、オブジェクトリポジトリの雛形から作成します。[ファイル(F)|新規作成(N)] で、「スレッドオブジェクト」を選択し、TMsgThreadという名前の派生クラスを作成します。

Hide image
fig_2

図2

TMsgThreadは、表示先のPanelと表示メッセージ文字列を保持します。コンストラクタは、これらの変数に与える値を引数とします。Executeメソッドからは、パネルのキャプションを変更するChangePanelメソッドを呼びます。

   type
     TmsgThread = class(TThread)
     private
        FPanel:   TPanel;
        FMsgStr:  String;
        FPos:     Integer;
        FLen:     Integer;
        Procedure ChangePanel;
       { Private 宣言 }
     protected
       procedure Execute; override;
     public
       constructor Create( panel: Tpanel; MsgStr: String); virtual;
   end;

interface部のuses節には、TPanelを使うために、ExtCtrlsを追加しておきます。また、Windows APIが使用できるようにWindowsも追加します。

    コンストラクタ

TMsgThreadのコンストラクタは、以下の通りです。

   constructor TmsgThread.Create( panel: Tpanel; MsgStr: String); virtual;
   begin
        FPanel  := Panel;
        FmsgStr := MsgStr;
        FLen    := Length(FmsgStr);
        inherited Create(False);
        FreeOnTerminate := True;
   end;

予約語inheritedによって、TThreadのCrateメソッドを呼び出しています。引数のFalseは、スレッドが生成後直ちに実行されることを示しています。

FreeOnTerminateプロパティをTrueに指定し、スレッドが終了したときに、スレッドオブジェクトが破棄されるように設定します。

    Executeメソッド

パネルの文字列を変更するためのコードは、Executeメソッドをオーバライドして記述します。

   { Executeメソッドから呼ばれるメソッド }
   Procedure TmsgThread.ChangePanel;
   begin
       if FPos <= FLen then
           FPanel.caption := Copy(FMsgStr, 1, FPos);
       Inc(FPos);
       if FPos > FLen + 60 then
       begin
           FPos := 0;
           FPanel1.Caption := '';
       end;
   end;
   {オーバーライドされたExecuteメソッド }
   procedure TmsgThread.Execute;
   begin
     FPos := 0;
     while not Terminated do
     begin
         Synchronize(ChangePanel);
         Sleep(80);
     end;
   end;

TerminateプロパティがTrueになるまで、つまりスレッドが中断されるまで、パネル表示の変更を行ないます。実際の表示の変更は、ChangePanelメソッドで行なっています。パネルには、1文字ずつ文字が表示されていき、やがて全ての文字列が表示され、しばらくその状態を保つようにしています。

    Synchronizeを使用する理由

Executeメソッドは、実際の処理をChangePanelメソッドをSynchronizeメソッドを経由して呼び出して行なっています。なぜ、単純にExecuteメソッドの中で処理したり、以下のように直接ChangePanelメソッドを呼び出せないのでしょうか。

   procedure TMsgThread.Execute;
   begin
     FPos := 0;
     while not Terminated do
       ChangePanel; { 不適切な呼び出し方 }
   end;

ChangePanelメソッドは、PanelというVCLコンポーネントを操作します。Delphiアプリケーションでは、 VCLコンポーネントを使用できるのはメインスレッドだけです。しかし、Synchronizeメソッドは、このメソッド経由で呼び出されたメソッドが完了するまで、メインスレッドの処理を待機させ、マルチスレッドでVCLが使用できるようにします。

ただし、以下のような方法でSynchronizeメソッドを用いると、スレッドの中断に応答できないばかりでなく、マルチスレッドの意味が失われてしまいます。

   procedure TMsgThread.ChangePanel;
   begin
   { Synchronizeによって呼び出される
      メソッドとしては不適切 です !! 
      メインスレッドは永久に待機してしまいます }
     FPos := 0;
     while not Terminated do begin
       if FPos <= FLen then
         FPanel.Caption := Copy(FMsgStr, 1, FPos)
       else if FPos = FLen + 1 then
         FPanel.Caption := FMsgStr;
       Inc(FPos);
       if FPos > FLen + 60 then begin 
         FPos := 0;
         FPanel1.Caption := '';
       end;
       Sleep(80);
     end:
   end;
   
   procedure TMsgThread.Execute;
   begin
   { ChangePanelがループなので、
    メインスレッドに復帰できない }
     Synchronize(ChangePanel);
   end;

    スレッドの生成

フォームに貼り付けたボタンのOnClickイベントにスレッドオブジェクトを生成するメソッドを記述します。

   procedure TForm1.Button1Click(Sender: TObject);
   begin
     TMsgThread.Create(
      Panel1, 'BORLAND  Delphi 3.1J');
   end;

このユニットでTMsgThreadオブジェクトが使用できるように、[ファイル(F)|ユニットを使う(U)...]メニューを選択してTMsgThreadが定義されたユニットを、implementation部のuses節に追加します。

以上でサンプルアプリケーションの出来上がりです。プログラムを実行し、ボタンを押してみて下さい。パネルの文字が変わっていきますが、同時にメモの内容を編集できるでしょう。

    3. マルチスレッドの使用例(2)

    ディレクトリツリーの表示

次の例は、もう少し複雑です。ハードディスクのディレクトリツリーを全て表示するのは、大変時間のかかる処理です。エクスプローラなどのアプリケーションでは、サブディレクトリをオープンしたときに、その下のディレクトリを検索することで高速化しています。ここでは、マルチスレッドを使用して全てのディレクトリツリーを検索し、検索中でも既に表示されたディレクトリを選択できるようにしてみましょう。

Hide image
fig_3

図3

フォームには、TreeViewを配置します。TreeViewには、ディレクトリの構造を表示させます。

Hide image
fig_4

図4

ディレクトリアイコンを格納するImageListも必要です。ImageListには、Delphi 2.0J/3.1Jをインストールしたディレクトリ下のImages\Defaultにある、outclose.bmpとoutopen.bmpを登録します。そして、TreeViewのImageListプロパティに、配置したImageListを指定します。ディレクトリの検索を実行するボタンも配置します。

    検索スレッドオブジェクトの作成

前節と同じ方法で、TTreeThreadという名前の派生クラスを作成します。TTreeThreadは、TreeViewのツリーデータであるTreeNodesとドライブ名を保持します。コンストラクタは、これらの変数に与える値を引数とします。Executeメソッドでは、このドライブのサブディレクトリを検索し、TreeNodesに登録します。

   type
     TTreeThread = class(TThread)
     private
         FNode:        TTreeNode;
         FParentNode:  TTreeNode;
         FNodes:       TTreeNodes;
         FName:        String;
         FDrive:       String;
         procedure AddANode;
         procedure AddTree( Node: TTreeNode; Path: String);
     protected
       procedure Execute; override;
     Public
       constructor Create( Nodes: TTreeNodes; Drive: String); Virtual;
   end;

interface部のuses節には、SysUtils、ComCtrls、 StdCtrlsを追加します。

    コンストラクタ

TTreeThreadのコンストラクタでは、登録先のTreeNodesをFNodes変数に代入し、検索対象となるドライブ名文字列をFDrive変数に代入しています。

   constructor TTreeThread.Create( Nodes: TTreeNodes; Drive: String);
   begin
       FNodes := Nodes;
       FDrive := Drive;
       inherited create(False);
       FreeOnTerminate := True;
   end;

実際のTreeNodesは、TreeViewのItemsプロパティです。このスレッドオブジェクトでは、TreeViewのItemsプロパティで示されるTreeNodesに項目を追加するだけで、TreeViewを直接操作することはありません。

    AddANodeメソッド

TreeNodesに項目を追加すると、結果としてTreeViewの表示が更新されます。TreeNodesへのアクセスは、VCLコンポーネントへのアクセスなので、Synchronizeメソッド経由で呼び出さなければなりません。ひとつの項目を追加するメソッドとしてAddANodeを作成し、これをSynchronizeメソッド経由で呼び出すようにします。

   procedure TTreeThread.AddANode;
   begin
      FNode := FNodes.AddChild(FParentNode, FName);
      FNode.ImageIndex := 0;
      FNode.SelectedIndex := 1;
   end;

FParentNodeとFNameには、あらかじめ値を設定しておかなければなりません。あらたに追加された項目は、FNodeにセットされるので、このメソッドを呼び出したあとで使用できます。

前節で説明したようにSynchronizeメソッドは、メインスレッドの処理を待機させるので、 Synchronize経由で呼び出すメソッドは、このような小さな処理単位にしておかなければなりません。

    再帰的処理

サブディレクトリを検索し、さらにそのサブディレクトリを次々に検索していくような処理には、通常、再帰関数(手続き)を使用します。再帰関数とは、その関数の中で自分自身を呼び出す関数です。永久に自分自身を呼び出していたのでは無限ループになってしまいますから、ある条件に合致したときだけ呼び出すようにします。この場合は、サブディレクトリが存在したときです。

再帰的処理を用いたディレクトリ検索メソッドAddTreeは、次のようになります。このメソッドは、Executeメソッドから呼ばれます。Executeメソッドでは、Terminatedプロパティを監視しながら処理を行なわなければなりませんでした。このメソッドは、再帰的に呼ばれ、長い処理になりますから、Terminatedプロパティの変更に直ちに応答するには、この関数内でプロパティを監視していなければなりません。

   procedure TTreeThread.AddTree( Node: TTreeNode; Path: String);
   var
     rec: TSearchRec;
     rval: Integer;
   begin
     rval := FindFirst(Path+'*.*', faDirectory, rec);
     while(rval = 0) and not terminated do
     begin
         if ((rec.Attr and faDirectory) > 0) and (rec.name <> '.') and
              (rec.name <> '..' ) then
         begin
             FName := rec.name;
             FParentNode := Node;
             Synchronize(AddANode);
             AddTree(FNode, Path + rec.Name + '\');
         end;
         rval := FindNext(rec);
     end;
     findclose(rec);
   end;

AddANodeメソッドは、このメソッドからSynchronizeメソッド経由で呼ばれます。AddANodeを呼んだあとでは、FNodeに追加した項目のTreeNodeが保持されているので、これを引数としてAddTreeを呼びます。再帰的に呼ばれたAddTreeでは、引数として渡されたノードを親ノードとしてサブディレクトリ項目を追加していきます。

FindFirst、FindNext...は、指定されたディレクトリ内で、指定されたファイル名と属性に一致するエントリを検索する関数です。これらの関数を用いたときは、FindClose手続きによって、割り当てられたメモリを解放しなければなりません。

    Executeメソッド

Executeメソッドは簡単です。親ノードをnilに設定して、ルート項目を追加し、AddTreeメソッドを呼び出すだけです。AddTreeから復帰するのは、全てのサブディレクトリを検索しTreeNodesに登録したか、スレッドが中断された場合です。

   procedure TTreeThread.Execute;
   begin
       FParentNode := nil;
       FName := FDrive;
       Synchronize(AddANode);
       AddTree(FNode,FDrive);
   end;

    検索スレッドの生成

前節と同じように、フォームに貼り付けたボタンのOnClickイベントにスレッドオブジェクトを生成するメソッドを記述し、スレッドオブジェクトが定義されたユニットをuseseに追加します。

   procedure TForm1.Button1Click(Sender: TObject);
   begin
     TTreeThread.Create(TreeView1.Items, 'c:\');
   end;

Hide image
fig_5

図5

プログラムを実行しボタンを押すと、ディレクトリの検索を開始します。検索には時間がかかりますが、検索中でもTreeViewの項目を選択したり、ツリーを開いたりできるはずです。スレッドオブジェクトは、検索を終了すると自動的に破棄されます。

    マルチスレッド処理の実装

この例では、再帰的処理を行ないましたが、次の2点に注意すれば、どのような処理でもマルチスレッドにすることができます。

  • Terminatedプロパティに直ちに応答できる
  • VCLコンポーネントにアクセスするときは、Synchronizeメソッドを用いる

そして、Synchronizeメソッドがメインスレッドを待機させるという点に注意して、Synchronize経由で呼ばれるメソッドの処理は、最小限の単位に限定します。また、このメソッドは引数を持つことができないので、このメソッドに渡したいデータは、あらかじめ、スレッドオブジェクトのprivateメンバ変数に格納しておきます。

次のような処理は、マルチスレッドオブジェクトに実装することが困難です。

  • 長い処理がVCLコンポーネントのメソッドである
  • 処理中に他のスレッドによって処理中のデータが変更される恐れがある

例えば、RichEditのPrintメソッドをマルチスレッドで実行しようとしても、このメソッドはSynchronize経由でしか呼び出せないので、マルチスレッドの効果は得られません。Printメソッドを自作して、RichEditにアクセスする部分だけを切り出してSynchronize経由で呼び出しても、処理中にRichEditの内容が変更されることに対策がありません。

マルチスレッドを採用するかどうかは、難しい選択かもしれません。処理を明確に区分し、お互いが衝突して予期しない状態にならないように、十分整理されていなければならないでしょう。

しかし、場合によっては、マルチスレッドによって処理の独立性が高まり、アプリケーションの設計がすっきりすることもあります。また、C/S環境では、より効率的な処理が行なえることもあります。マルチスレッドの性格をよく理解し、十分なテストを行なってから実際のアプリケーションに採用して下さい。