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 の派生
スレッドオブジェクトを作るためのウィザードは [ファイル | 新規作成 | その他]
から呼び出せます。
スレッドクラス名を TMyThread
にして、
自動生成されたユニットが次のようなコードになります。
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()
) メソッドが呼ばれるまで実行されません。 - 三番目の書式 4 は
ReservedStackSize
を指定する事でスタックサイズを設定できますが、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;
スレッドオブジェクトは実行後に自動破棄されますが、スレッド実行中にアプリケーションを終了させた場合には普通にメモリリークします。
スレッドが実行中の場合には適切な中止処理を行う必要があります。
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:
- System.Classes.TThread.Synchronize (DocWiki)
- Delphi での無名メソッド (DocWiki)
- D6DLLSynchronizer for Delphi 6 and 7 (Code Central)
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 アプリケーションを作って確認してみます。フォームにはメモとボタンが一つずつあります。
ボタンのイベントハンドラに無名スレッドを記述します。
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
だけが表示されていると思います。
ドキュメントには次のように書かれています。
メイン スレッドは、最終的にキューに入っているすべてのメソッドを処理します。
嘘ではないですが、それはスレッドが終了していない場合の話です。つまり、先にスレッドが終了してしまったので、それ以降のメソッド呼び出しが削除 (キャンセル) されてしまったのです。
・解決方法 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()
で追加された呼び出しはすべて処理される事になります。
Queue() はメッセージループを利用するため、コンソールアプリケーションでは使用できません。
See also:
- System.Classes.TThread.Queue (DocWiki)
- Delphi アルゴリズムトレーニング 第2回 単純なキューと循環キュー (@IT)
- Delphi Queue and Synchronize (StackOverflow)
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〕)。
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\Threads
や Delphi\RTL\Threads
、Object Pascal\RTL\Threads
等にあります。
この Threads
デモは XE ~ XE6 のサンプルリポジトリに残っています。
サンプルリポジトリから Threads
デモを開くには [ファイル | バージョン管理リポジトリから開く...]
を選択し、バージョン管理システムとして Subversion
を選びます。
リポジトリの場所とコピー先には次のように指定します。
項目 | 値 |
---|---|
リポジトリの場所 | https://svn.code.sf.net/p/radstudiodemos/code/branches/RadStudio_XE6/Object Pascal/RTL/Threads/ |
コピー先 | 既存の任意のフォルダ (ここでは C:\Work\Threads) |
[OK] ボタンを押すとダウンロードが開始され、ダウンロードが終わるとプロジェクトを選択するダイアログが開くので、thrddemo.dpr
を選択します。
ユニット SortThds.pas
の uses に Types
を追加すると、ヒントやワーニングなしでコンパイルできます。
unit SortThds;
interface
uses
Classes, Types, Graphics, ExtCtrls;
...
See also:
参考
- Delphi Tips - マルチスレッドアプリケーション (EDN)
- スレッド管理ルーチン (DocWiki)
- Delphi でのマルチスレッドプログラミング (第11回 エンバカデロ・デベロッパーキャンプ【A2】)
- 正しい GUI の作り方 (第16回 エンバカデロ・デベロッパーキャンプ【2C】)
- Delphi でキカイを制御する (第16回 エンバカデロ・デベロッパーキャンプ【2F】)
- TThread の Synchronize と Queue について (山本隆の開発日誌)
- C++Builder 2009 のスレッドの基本的な使い方のまとめ (山本隆の開発日誌)
- Delphi の TThread.CreateAnonymousThread と無名メソッドを使うと、簡単なスレッド処理なら手軽にかける (山本隆の開発日誌)
- C++ Builder / TThread > スレッドが終了時にソフトを終了する実装 (Qiita: @7of9)
- C++ Builder / TTreahd > スレッド終了後の処理 (Qiita: @7of9)
- C++ Builder > TThread:WaitFor() を使うときの注意 > FreeOnTerminate を false にしておく (Qiita: @7of9)
- C++ Builder XE4 > TThread > Suspended() 後の Start() > 実行中または一時停止中のスレッドに対しては Start を呼び出せません (Qiita: @7of9)
- C++ Builder XE4 > TThread > 「実行中または一時停止中のスレッドに対しては Start を呼び出せません。」 > : TThread(/* CreateSuspended= */ true) で作ったスレッドの new 直後の Start は問題ない (Qiita: @7of9)
- Delphi で指定フォルダ以下のファイル一覧を取得する (Qiita: @SequencePalladium)
- Delphi TMessageManager (Qiita: @furudoi)
- クラスメソッド版 TThread.Synchronize の使いどころ。 (Swanman's Horizon)
索引
[ ← 0. はじめに ] [ ↑ 目次へ ] [ → 2. クリティカルセクションとロック ]
-
Resume()
およびSuspend()
メソッドは Delphi 2010 以降で非推奨となっており、使用すると ワーニング W1000 が発生します。.NET においてもこれらのメソッドは非推奨となっています (Resume() および Suspend())。 ↩ -
パラメータのないコンストラクタは Delphi XE 以降で利用可能です。 ↩
-
Start()
メソッドは Delphi 2010 以降で利用可能です。 ↩ -
スタックサイズを指定可能なコンストラクタは Delphi XE 以降で利用可能です。 ↩
-
クラスメソッド
NameThreadForDebugging()
は Delphi 2010 以降で利用可能です。 ↩ -
ウィザードの [名前付きスレッド] は Delphi 7 以降で利用可能です。 ↩
-
無名メソッドに対応した
Synchronize()
は Delphi 2009 以降で利用可能です。 ↩ -
CreateAnonymousThread()
は Delphi XE 以降で利用可能です。 ↩ -
クラスメソッドの
Synchronize()
は Delphi 7 以降で利用可能です。Delphi 2007 以降ではStaticSynchronize()
というクラスメソッドもありますが、単にSynchronize()
を呼んでいるだけであり、現在では非推奨としてマークされています。 ↩ -
Queue()
メソッドは Delphi 2005 以降で利用可能です。Delphi 2007 以降ではStaticQueue()
というクラスメソッドもありますが、単にQueue()
を呼んでいるだけであり、現在では非推奨としてマークされています。 ↩ -
TThread.Sleep()
メソッドは Delphi XE 以降で利用可能です。 ↩ -
SpinWait()
メソッドは Delphi 2010 以降で利用可能です。 ↩ -
CheckSynchronize()
メソッドは Delphi 6 以降で利用可能です。 ↩ -
Threads
デモは XE6 まで収録されていました。 ↩