14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DelphiAdvent Calendar 2021

Day 7

<1> スレッドオブジェクト (Delphi コンカレントプログラミング)

Last updated at Posted at 2021-12-06

1. スレッドオブジェクト

TThread クラスは、アプリケーション内で別の実行スレッド (バックグラウンドスレッド) を作成できるようにする抽象クラスで、System.Classes で定義されています。BeginThread()EndThread() が内部で使われています。

最初期のこのクラスは Windows API をラップしたものでした。

Delphi Windows API
TThread.Create() コンストラクタ System.BeginThread() = CreateThread()
TThread.Priority プロパティ GetThreadPriority() / SetThreadPriority()
TThread.Resume() メソッド ResumeThread()
TThread.Suspend() メソッド SuspendThread()

最近の Delphi の TThread クラスはマルチプラットフォーム対応ですが、Resume() / Suspend() メソッドは非推奨とマークされている 1 上に、Windows / macOS プラットフォーム向けにしか実装されていません。

BeginThread() を実行するか、スレッドオブジェクトが生成されると、IsMultiThread グローバル変数が True になり、メモリマネージャがマルチスレッドをサポートするようになります。Windows API を直接叩いた場合には IsMultiThread を手動で True に変更する必要があります。

1.1. TThread の使い方

TThread は抽象クラスなので、派生して (サブクラス化して) 使います。

1.1.1. TThread の派生

スレッドオブジェクトを作るためのウィザードは [ファイル | 新規作成 | その他] から呼び出せます。

image.png

スレッドクラス名を TMyThread にして、

image.png

自動生成されたユニットが次のようなコードになります。

unit2.pas
unit Unit2;

interface

uses
  System.Classes;

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

implementation

{ TMyThread }

procedure TMyThread.Execute;
begin
  { スレッドとして実行したいコードをここに記述してください }
end;

end.

スレッドとして実行したいコードは Exceute() メソッドに記述します。

慣れてくると、ウィザードを使わずにスレッドクラスを書けるようになりますが、〔Shift〕+〔Ctrl〕+〔C〕でクラス補完すると TThread.Execute() メソッドに inherited が追加されてしまいます。TThread.Execute() は抽象メソッドなので、inherited の記述は不要です。

procedure TMyThread.Execute;
begin
  inherited; // 不要
  
end;

See also:

1.1.2. スレッドオブジェクトの実行

スレッドクラスはインスタンス化したと同時に実行されます (Execute メソッドが呼ばれます)。

    constructor Create; overload;
    constructor Create(CreateSuspended: Boolean); overload;
{$IF Defined(MSWINDOWS)}
    constructor Create(CreateSuspended: Boolean; ReservedStackSize: NativeUInt); overload;
{$ENDIF MSWINDOWS}
  • 最初の書式 2 はインスタンス化したと同時に Execute メソッドが呼ばれます。
  • 二番目の書式は CreateSuspended パラメータを True に設定する事により、スレッドを中断した状態 (サスペンド状態) で作成します。Execute メソッドは Start() 3 (または Resume()) メソッドが呼ばれるまで実行されません。
  • 三番目の書式 4ReservedStackSize を指定する事でスタックサイズを設定できますが、Windows 専用です。

Execute() メソッドを抜けると、スレッドが終了してしまうので、繰り返し処理したい場合には Terminated プロパティをチェックしながらループします。

procedure TMyThread.Execute;
begin
  while not Terminated do 
  begin
    ...
  end;
end;

古い Delphi ではサスペンド状態で作成したスレッドオブジェクトを Resume() することなく破棄するとメモリリークを起こしていました。

Resume() メソッドはサスペンド状態で作成されたスレッドを開始するためにのみ使うべきです。また、この用途のための Resume() はマルチプラットフォーム対応となっています。

See also:

1.1.3. FreeOnTerminate

スレッド実行完了時に自動でスレッドオブジェクトを破棄したい場合には、FreeOnTerminate プロパティを True に設定します。

  MyThread := TMyThread.Create(True);
  MyThread.FreeOnTerminate := True;
  MyThread.Start;

FreeOnTerminate プロパティを True に設定するタイミングは Execute() メソッドが終了する前であればどこでも構わないため、スレッドをサスペンド状態で作成したくないのなら、Execute() メソッドの先頭で記述する事もできます。

procedure TMyThread.Execute;
begin
  FreeOnTerminate := True;
  ...
end;

スレッドオブジェクトは実行後に自動破棄されますが、スレッド実行中にアプリケーションを終了させた場合には普通にメモリリークします。

image.png

スレッドが実行中の場合には適切な中止処理を行う必要があります。

See also:

1.1.4. OnTerminate イベント

TThread にはスレッドが終了した時に呼ばれる OnTerminate イベント (TNotifyEvent 型) があります。

  MyThread := TMyThread.Create(True);
  MyThread.FreeOnTerminate := True;
  MyThread.OnTerminate := MyThreadTerminate;
  MyThread.Start;

OnTerminate のイベントハンドラはメインスレッドで実行されます。

See also:

1.1.5. 明示的なスレッドオブジェクトの破棄

FreeOnTerminate = False なスレッドオブジェクトを明示的に破棄するには FreeAndNil() を使います。

  FreeAndNil(MyThread);

丁寧な破棄は次のようになります。

  MyThread.Terminate;
  MyThread.WaitFor;
  FreeAndNil(MyThread);

結局の所、いきなり FreeAndNil() しても上記処理が実行されます。

MyThread.Free で破棄するよりも、FreeAndNil() を使って破棄した方がトラブルは少なくなると思います。

1.1.6. スレッドオブジェクトの終了 (中止)

・FreeOnTerminate = True なスレッドオブジェクトの終了 (中止)

Terminate メソッドを呼びます。

  if Assigned(MyThread) then
    MyThread.Terminate;

この Terminate メソッドは、基本的に Terminated プロパティを True に設定しているだけなので、Execute() メソッドには (Terminated プロパティをチェックしながら) スレッドを正しく終わらせられるコードを記述する必要があります。

・FreeOnTerminate = False なスレッドオブジェクトの終了 (中止)

FreeAndNil() で破棄するのが簡単だと思います。

  FreeAndNil(MyThread);

1.1.7. スレッドにパラメータを渡す

普通のクラスと同じようにコンストラクタを定義します。

  public
    { public 宣言 }
    constructor Create(Index: Integer);

...

constructor TMyThread.Create(Index: Integer);
begin
  inherited Create(False);
  Self.FreeOnTerminate := True;

  // 処理

end;

1.1.8. スレッドに名前を付ける

デバッグ用途でスレッドに名前を付けたい場合には、クラスメソッド NameThreadForDebugging() を使います 5。ウィザードで 名前付きスレッド にチェックを入れると同等のコードが吐かれます 6

procedure TMyThread.Execute;
begin
  NameThreadForDebugging('MyThread1');

  while not Terminated do 
  begin
    ...
  end;
end;

古い Delphi のウィザードでは NameThreadForDebugging() と同等の SetName() メソッドが自動生成されていました。

unit Unit2;

interface

uses
  Classes {$IFDEF MSWINDOWS} , Windows {$ENDIF};

type
  TMyThread = class(TThread)
  private
    procedure SetName;
  protected
    procedure Execute; override;
  end;

implementation

{$IFDEF MSWINDOWS}
type
  TThreadNameInfo = record
    FType: LongWord;     // 0x1000 にすること。
    FName: PChar;        // (ユーザー空間での) 名前文字列へのポインタ
    FThreadID: LongWord; // スレッド ID (呼び出しスレッドは -1)
    FFlags: LongWord;    // 将来のために予約。0 にすること。
  end;
{$ENDIF}

{ TMyThread }

procedure TMyThread.SetName;
{$IFDEF MSWINDOWS}
var
  ThreadNameInfo: TThreadNameInfo;
{$ENDIF}
begin
{$IFDEF MSWINDOWS}
  ThreadNameInfo.FType := $1000;
  ThreadNameInfo.FName := 'MyThread';
  ThreadNameInfo.FThreadID := $FFFFFFFF;
  ThreadNameInfo.FFlags := 0;

  try
    RaiseException( $406D1388, 0, sizeof(ThreadNameInfo) div sizeof(LongWord), @ThreadNameInfo );
  except
  end;
{$ENDIF}
end;

procedure TMyThread.Execute;
begin
  SetName;
  { ToDo : スレッドとして実行したいコードをこの下に記述してください }
end;

end.

See also:

1.1.9. Synchronize() メソッド

VCL / FMX コンポーネントはメインスレッドで動いているので、バックグラウンドスレッドからコンポーネントを更新したい場合には、Synchronize() メソッドを使い、パラメータで指定された手続きをメインスレッド内で実行します。

procedure TMyThread.Hoge;
begin
  // 何かしらの VCL / FMX コンポーネントを操作する処理
end;

procedure TMyThread.Execute;
begin
  NameThreadForDebugging('MyThread1');

  while not Terminated do 
  begin
    ...
    Synchronize(Hoge);
    ...
  end;
end;

指定した手続きがメインスレッド内で実行されている間はバックグラウンドスレッドは停止していますので、頻繁に呼び出すべきではありません。また、Synchronize() はパラメータとして

  • 手続きのメソッドポインタ型 (of object)
  • 手続きのメソッド参照型 (reference to)

を受け付けるため、ここに無名メソッドを指定する事ができます 7

procedure TMyThread.Execute;
begin
  NameThreadForDebugging('MyThread1');

  while not Terminated do 
  begin
    ...
    Synchronize(
      procedure
      begin
        // 何かしらの VCL / FMX コンポーネントを操作する処理
      end
    );
    ...
  end;
end;

Synchronize() はメッセージループを利用するため、コンソールアプリケーションでは使用できません。

Delphi 6 以降、Synchronize() は DLL 内で機能しなくなりました。

See also:

1.1.10. CreateAnonymousThread() メソッド

TThread クラスには CreateAnonymousThread() というクラスメソッドが用意されています 8。手続きのメソッド参照型をパラメータとして受け付けるこのメソッドは、内部的に生成された TThread 派生クラスのインスタンスを返します。スレッドオブジェクトは

  • サスペンド状態
  • FreeOnTerminate = True

で作られるため、次のような使い方になります。

  TThread.CreateAnonymousThread(
    procedure
    begin
      // 何かしらの処理
    end
  ).Start;

Synchronize() はどうするのかと言うと、クラスメソッドの方の Synchronize() を使います 9

  TThread.CreateAnonymousThread(
    procedure
    begin
      // 何かしらの処理

      TThread.Synchronize(TThread.CurrentThread,
        procedure
        begin
          // 何かしらの VCL / FMX コンポーネントを操作する処理
        end);
    end
  ).Start;

OnTerminate はどうするのかと言うと、一旦変数に取ります。

procedure TForm1.MyThreadTerminate(Sender: TObject);
begin
  // 何かしらのスレッド終了時処理
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  var MyThread := TThread.CreateAnonymousThread(
    procedure
    begin
      // 何かしらの処理

      TThread.Synchronize(TThread.CurrentThread,
        procedure
        begin
          // 何かしらの VCL / FMX コンポーネントを操作する処理
        end);
    end
  );
  MyThread.OnTerminate := MyThreadTerminate;
  MyThread.Start;
end;

こうなってしまうと、普通に派生クラス (サブクラス) を書いた方が可読性が高そうです。

See also:

1.1.11. Queue() メソッド

Synchronize()Queue() 10 で置き換えると、バックグラウンドスレッドがパラメータで指定された手続きの実行を待たなくなります。

ただし、Queue() に指定した手続きの呼び出しは、スレッドが終了すると削除されてしまうので注意が必要です。新規に VCL アプリケーションを作って確認してみます。フォームにはメモとボタンが一つずつあります。

image.png

ボタンのイベントハンドラに無名スレッドを記述します。

procedure TForm1.Button1Click(Sender: TObject);
begin
 TThread.CreateAnonymousThread(
   procedure
   begin
     TThread.Queue(TThread.Current,
                   procedure
                   begin
                     Memo1.Lines.Clear;
                     Memo1.Lines.Add('start');
                   end);
     TThread.Sleep(2000);
     TThread.Queue(TThread.Current, 
                   procedure
                   begin
                     Memo1.Lines.Add('end');
                   end);
   end
 ).Start;
end;

ボタンを押すとメモに start が表示され、2 秒後に end が表示される...ハズですが、メモにはいつまで経っても start だけが表示されていると思います。

image.png

ドキュメントには次のように書かれています。

メイン スレッドは、最終的にキューに入っているすべてのメソッドを処理します。

嘘ではないですが、それはスレッドが終了していない場合の話です。つまり、先にスレッドが終了してしまったので、それ以降のメソッド呼び出しが削除 (キャンセル) されてしまったのです。

・解決方法 1

Queue() の最初のパラメータに nil を指定します。

procedure TForm1.Button1Click(Sender: TObject);
begin
 TThread.CreateAnonymousThread(
   procedure
   begin
     TThread.Queue(TThread.Current,
                   procedure
                   begin
                     Memo1.Lines.Clear;
                     Memo1.Lines.Add('start');
                   end);
     TThread.Sleep(2000);
     TThread.Queue(nil,                      // <- nil を指定 (最後の一つだけで構わない)
                   procedure
                   begin
                     Memo1.Lines.Add('end');
                   end);
   end
 ).Start;
end;

TThread.Queue() の最初のパラメータが nil でない場合、キューに入れられた呼び出しがパラメータで指定されたスレッドと関連付けられ、スレッドが終了すると、まだ処理されていない呼び出しは TThread.RemoveQueuedEvents() によりキャンセルされます。

・解決方法 2

ダミーの Synchronize() をキューの最後に追加します。

procedure TForm1.Button1Click(Sender: TObject);
begin
 TThread.CreateAnonymousThread(
   procedure
   begin
     TThread.Queue(TThread.Current,
                   procedure
                   begin
                     Memo1.Lines.Clear;
                     Memo1.Lines.Add('start');
                   end);
     TThread.Sleep(2000);
     TThread.Queue(TThread.Current,
                   procedure
                   begin
                     Memo1.Lines.Add('end');
                   end);
     TThread.Synchronize(TThread.Current, procedure begin end); // <- ダミーのSynchronize() を追加 
   end
 ).Start;
end;

Synchronize() がキューの最後に追加されると待ちが発生するため、結果的に Queue() で追加された呼び出しはすべて処理される事になります。

image.png

Queue() はメッセージループを利用するため、コンソールアプリケーションでは使用できません。

See also:

1.1.12. Sleep() クラスメソッド

TThread.Sleep() 11 は指定したミリ秒数だけプログラム実行を停止させます。

現在の実装では System.Classes.TThread.Sleep() メソッドと System.SysUtils.Sleep() 手続きは同じ処理内容になっているため、単に Sleep() と書いても (どちらが使われても) 問題ありません。将来、何かしらの変更が発生する事も考慮して、記事中では TThread.Sleep() を使っています。

この Sleep を (別のスレッドから) 途中で終了させるメソッドも現在の所では用意されていません。

See also:

1.1.13. SpinWait() クラスメソッド

TThread.SpinWait() 12 は指定したスピンループ分だけプログラム実行を遅延させます。

空の for ループを回して時間調整するようなものです。

See also:

1.1.14. WaitFor() メソッド

WaitFor() メソッドを呼び出すとスレッドが終了するまで待ちます。

WaitFor() メソッドが返す値は ReturnValue プロパティの値です。ReturnValue はスレッドにおける Result 変数のようなものです。

procedure TForm1.Button1Click(Sender: TObject);
var
  MyThread: TMyThread;
  rv: Integer;
begin
  MyThread := TMyThread.Create;
  rv := MyThread.WaitFor; // スレッドが終了するのを待つ
  FreeAndNil(MyThread);
  ShowMessage(IntToStr(rv));
end;
  • WaitFor() メソッドは FreeOnTerminate プロパティが True の時には使えません。
  • WaitFor() メソッドはスレッドが終了するまで、呼び出し元のスレッドをブロックします。つまりメインスレッドで実行された場合、WaitFor() で待つ間はフォームを動かせなくなります。

See also:

1.1.15. コンソールアプリケーションでの Synchronize / Queue

先述の通り、コンソールアプリケーションではメッセージループがないため、コンソールアプリケーションでは Synchronize() / Queue() が動作しません。

次のコンソールアプリケーションを実行しても、何も表示されません (中断は〔Ctrl〕+〔C〕)。

ApplePen.dpr
program ApplePen;
{$APPTYPE CONSOLE}
uses
  System.Classes, System.Threading;

type
  TMyThread = class(TThread)
  private
    FData: string;
    procedure MyThreadTerminate(Sender: TObject);
  protected
    procedure Execute; override;
  public
    constructor Create(s: String);
  end;

var
  TermCount: Integer;

constructor TMyThread.Create(s: String);
begin
  inherited Create(True);
  FreeOnTerminate := True;
  OnTerminate := MyThreadTerminate;
  FData := s;
end;

procedure TMyThread.Execute;
begin
  Synchronize(procedure begin Writeln(FData) end);
end;

procedure TMyThread.MyThreadTerminate(Sender: TObject);
begin
  Inc(TermCount);
end;

begin
  TermCount := 0;

  TMyThread.Create('I have a pen.').Start;
  TMyThread.Create('I have an apple.').Start;
  TMyThread.Create('uh...Apple Pen.').Start;

  while TermCount < 3 do
    ;
end.

最後のループに CheckSynchronize() 13 を追加すると正しく動作するようになります。

  while TermCount < 3 do
    CheckSynchronize;

See also:

1.2. TThread のデモ

TThread のデモは、最近の Delphi には含まれていないようです 14。古い Delphi だと $(DELPHI)\DEMOS\ThreadsDelphi\RTL\ThreadsObject Pascal\RTL\Threads 等にあります。

この Threads デモは XE ~ XE6 のサンプルリポジトリに残っています。

サンプルリポジトリから Threads デモを開くには [ファイル | バージョン管理リポジトリから開く...] を選択し、バージョン管理システムとして Subversion を選びます。

image.png

リポジトリの場所とコピー先には次のように指定します。

項目
リポジトリの場所 https://svn.code.sf.net/p/radstudiodemos/code/branches/RadStudio_XE6/Object Pascal/RTL/Threads/
コピー先 既存の任意のフォルダ (ここでは C:\Work\Threads)

image.png

[OK] ボタンを押すとダウンロードが開始され、ダウンロードが終わるとプロジェクトを選択するダイアログが開くので、thrddemo.dpr を選択します。

image.png

ユニット SortThds.pasusesTypes を追加すると、ヒントやワーニングなしでコンパイルできます。

SortThds.pas
unit SortThds;

interface

uses
  Classes, Types, Graphics, ExtCtrls;

...

image.png

image.png

See also:

参考

索引

[ ← 0. はじめに ] [ ↑ 目次へ ] [ → 2. クリティカルセクションとロック ]

  1. Resume() および Suspend() メソッドは Delphi 2010 以降で非推奨となっており、使用すると ワーニング W1000 が発生します。.NET においてもこれらのメソッドは非推奨となっています (Resume() および Suspend())。

  2. パラメータのないコンストラクタは Delphi XE 以降で利用可能です。

  3. Start() メソッドは Delphi 2010 以降で利用可能です。

  4. スタックサイズを指定可能なコンストラクタは Delphi XE 以降で利用可能です。

  5. クラスメソッド NameThreadForDebugging() は Delphi 2010 以降で利用可能です。

  6. ウィザードの [名前付きスレッド] は Delphi 7 以降で利用可能です。

  7. 無名メソッドに対応した Synchronize() は Delphi 2009 以降で利用可能です。

  8. CreateAnonymousThread() は Delphi XE 以降で利用可能です。

  9. クラスメソッドの Synchronize() は Delphi 7 以降で利用可能です。Delphi 2007 以降では StaticSynchronize() というクラスメソッドもありますが、単に Synchronize() を呼んでいるだけであり、現在では非推奨としてマークされています。

  10. Queue() メソッドは Delphi 2005 以降で利用可能です。Delphi 2007 以降では StaticQueue() というクラスメソッドもありますが、単に Queue() を呼んでいるだけであり、現在では非推奨としてマークされています。

  11. TThread.Sleep() メソッドは Delphi XE 以降で利用可能です。

  12. SpinWait() メソッドは Delphi 2010 以降で利用可能です。

  13. CheckSynchronize() メソッドは Delphi 6 以降で利用可能です。

  14. Threads デモは XE6 まで収録されていました。

14
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?