Windows でマルチスレッドを実現するには CreateThread API などを駆使する必要があります。 Delphi ではこれらの API をカプセル化した TThread クラスが用意されています。今回はこの TThread クラスを利用した簡単なマルチスレッドアプリケーションの構築方法をご紹介します。
TThreadクラス
TThread クラスは単に CreateThread API をラップしているだけでなく、別スレッドから安全に VCL にアクセスする手段を提供しています。「安全にアクセスする」 とは、複数のスレッドが同一のメモリ領域をアクセスしないようにすることです。椅子取りゲームでふたりがひとつの椅子に座っちゃうようなことを防ぐ仕組みが TThread クラスにはあるのです。
スレッドとは?
スレッドとは実行されているプロセスの一部です。言い換えればプロセスとは1つ以上のスレッドの集まりです。アプリケーションにはメインスレッドと呼ばれるスレッドが1つ存在します。マルチスレッドでないアプリケーションにはこのメインスレッドしか存在しません。マルチスレッドとは1つのプロセス内に複数のスレッドが存在し、各スレッドが別々の処理を行う技術なのです。
簡単なサンプル
スレッドオブジェクトサンプル用フォームではさっそく TThread を使った簡単なマルチスレッドアプリケーションをつくってみましょう。このアプリケーションの仕様は、ボタンをクリックすると新しいスレッドが生成され、そのスレッドが数字を1から1000までカウントし、その途中経過をフォームに表示するというものです。このプロジェクトには1つのフォーム、その中にボタンとラベルを配置します。次に新規作成で ( Delphi 6 なら [ ファイル ] - [ 新規作成 | その他 ] ) スレッドオブジェクト選択し、TMyThread と言う名前で作成し、 Unit2 で保存します。

フォーム側の処理
まずフォームの interface 部の uses 節にスレッドのユニット名、この場合は Unit2 を追加します。これをしないとフォームからスレッドを呼び出す事ができません。ここに追加するのはクラス名ではなくユニット名、要するに保存したファイル名を記述するということに気をつけてください。

Form1 の uses 節
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Unit2;

次に TMyThread 型の変数をフォームの private に宣言します。これはここで使用するスレッド、この場合だと数をカウントする TMyThread というスレッドを MyThread と言う名前の変数で宣言していることになります。TButton 型のオブジェクトを Button1 で宣言するのと同じです。

Form1 の定義
type
  TForm1 = class(TForm)
    Label1: TLabel;
    Button1: TButton;
  private
    { Private 宣言 }
    MyThread : TMyThread;
  public
    { Public 宣言 }
  end;

これでつまらない準備作業は終わりです。次はボタンを押された場合の処理を記述します。ここでは

  1. 新しいスレッドを生成し、
  2. スレッド終了時のイベントと関連付け、
  3. 処理を実行させる

だけです。なんか難しそうですが書くものは単純です。

Form1 の Button1の OnClick
procedure TForm1.Button1Click(Sender: TObject);
begin
  //ここでスレッドを生成し、処理を実行している
  MyThread  := TMyThread.Create(False);

  //スレッド終了時の処理を割り当てる
  //MyThreadDone手続きはこの後でつくる
  MyThread.OnTerminate  :=  MyThreadDone;
end;

Create メソッドに False を渡すとスレッド生成後、すぐに主処理( Executeメソッド) を実行します。Execute メソッドについては後述します。次はスレッド終了時のイベントを作成します。名前は MyThreadDone インベントです。このイベントでは生成したスレッドオブジェクトを破棄し、処理が終了したことをメッセージボックスで知らせます。このイベントはオブジェクトインスペクタから作ることができないので procedure... から自分で記述します。

Form1 の MyThreadDone
procedure TForm1.MyThreadDone(Sender: TObject);
begin
  ShowMessage('End');  
end;

手動で追加したイベントは Type中に宣言する必要がありますので追加します。

Form1 の定義
type
  TForm1 = class(TForm)
    Label1: TLabel;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    MyThread : TMyThread;
    procedure MyThreadDone(Sender: TObject);
  public
    { Public 宣言 }
  end;

これでフォーム側のコードは完成です。フォーム側では使用するスレッドを準備し、ボタンを押す事によりそのスレッドを生成、実行するように定義しました。また、スレッドの処理が終了したらメッセージボックスを表示するように記述しました。

スレッド側の処理
次はスレッド側の処理です。TMyThread のコードを表示し、implemetation 部に uses 節を追加してください。ここにフォームのユニット名と SysUtils ユニットを追加します。

Unit2 の定義
unit Unit2;

interface

uses
  Classes;

type
  TMyThread = class(TThread)
  private
    { Private 宣言 }
  protected
    procedure Execute; override;
  end;

implementation

uses
  Unit1, SysUtils;

次に1から1000までカウントする際に使用する内部変数を定義します。名前は FCount で整数型で宣言しましょう。

TMyThread の定義
type
  TMyThread = class(TThread)
  private
    { Private 宣言 }
    FCount : integer;
  protected
    procedure Execute; override;
  end;

そして Execute メソッドに処理を記述します。この Execute メソッドはスレッドが生成された直後に実行されるメソッドです ( 引数により直後に実行するかどうかを指定できます ) 。処理内容は 1 から 1000 までカウントし、その途中経過をフォームのラベルに表示することでしたので・・・

TMyThread の実行内容 (これは間違い)
procedure TMyThread.Execute;
var
  i : integer;
begin
  for i :=  1 to  1000  do  begin
    Form1.Label1.Caption  :=  IntToStr(i);
  end;
end;

と、したいところでしょうが これは間違いです。この場合ラベル1というオブジェクトは直接、メインスレッドの状態を無視した他スレッドから操作されています。別スレッドはメインスレッドのオブジェクトを安全に操作する必要があります。では、安全にアクセスするにはどうしたらいいのでしょうか?それは、直接 VCL をアクセスする処理だけをメインスレッドに依頼するのです。それを実現するのが TThread クラスの Synchronize メソッドです。次のコードは正しい例です。

TMyThread の実行内容
procedure TMyThread.Execute;
var
  i : integer;
begin
  for i :=  1 to  1000  do  begin
    FCount  :=  i;
    Synchronize(CountUp);
  end;
end;

procedure TMyThread.CountUp;
begin
  //この処理はメインスレッドにより実行される
  Form1.Label1.Caption  :=  IntToStr(FCount);
end;

Synchronize メソッドには手続き名を引数で渡します。するとその手続きの処理だけはメインスレッド内部で処理されます。ですのでこの手続き内の処理を巨大なものにすると、マルチスレッドの意味が失われます。次の例は悪い例です。これだとカウントするループの処理は全てメインスレッドで処理されてしまいます。

TMyThread の実行内容 (これは失敗)
procedure TMyThread.Execute;
begin
  Synchronize( CountUp );
end
 
procedure TMyThread.CountUp;
var
  i  :  integer;
begin
  for  i  :=  1  to  1000  do  begin
    Form1.Label1.Caption  :=  IntToStr(i);
  end;
end;

最後にこの手続き CountUp を TMyThread クラスのメソッドとして宣言する必要がありますので追加します。

TMyThread の定義
type
  TMyThread = class(TThread)
  private
    { Private 宣言 }
    FCount : integer;
    procedure CountUp;
  protected
    procedure Execute; override;
  end;

実行
これで完成です。早速実行してみてください。ボタンを押すとラベルに1から数字がカウントアップされていくはずです。しかも処理中にも関わらずフォームの移動や右上の×ボタンで終了ができます。

実行中

Synchronize メソッドは先程も説明したとおりメインスレッドで実行されます。よってこのSynchronize メソッド実行中はメインのスレッドは停止しています。Synchronize メソッド内に記述するコードは極力少なくしておくことが大切です。



1997-2001 Delphi Acid Floor