Multithreaded Application Tutorial/ja

From Lazarus wiki
Jump to navigationJump to search

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) polski (pl) português (pt) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

日本語版メニュー
メインページ - Lazarus Documentation日本語版 - 翻訳ノート - 日本語障害情報

概要

このページでは Free Pascal と Lazarus を使ってマルチスレッドアプリケーションを書いたりデバッグしたりする方法について述べます。

もし、読者がマルチスレッディングに初めて触れる読者は、是非マルチスレッディングが本当に必要かどうかを「マルチスレッディングが必要ですか」節でお確かめください。そうすれば頭痛の種を激減することができるでしょう。

マルチスレッドアプリケーションは、2つかそれ以上のスレッドを作成、同時実行して作業を行うものです。そのうち一つは、メインスレッドと呼ばれるものです。メインスレッドはアプリケーションが起動する際、オペレーティングシステムによって生成されます。メインスレッドはユーザーインターフェイス、コンポーネントを更新する唯一のスレッドです。メインスレッドはかならずただ一つである必要があり、そうでないとアプリケーションは暴走します。

マルチスレッドプログラミングの基本的な考え方は、ユーザがメインスレッドを通して作業する傍ら、もう一つのスレッドを用いてある種の処理をバックグラウンドで行えるようにアプリケーションを組むという点です。

スレッドの他の使いかたとして、アプリケーションの応答速度を改善することが挙げられます。例えばあなたがアプリケーションをつくって、ユーザーがボタンを押して、アプリケーションが処理を開始したとします(すごく重たい処理です)。処理の間、スクリーンは凍りつき反応が無くなり、ユーザーはアプリケーションが落ちたと思うでしょう。よくないことです。もし、その重たい処理を2つ目のスレッドの中で実行するようにすれば、アプリケーションの応答性は保たれ、まるでそんな処理は行っていないかのように振舞います。このような場合には、スレッドを開始する前にフォームの処理を開始するボタンを使用不可能にしておくと、ユーザーがそれ以上の仕事を要求することを無くすことができ、好都合です。

また、サーバアプリケーションを作る際に、同時に大量のクライアントに返答しなければならない場合にも使えます。

マルチスレッディングが必要ですか

もし読者がマルチスレッディングにこれまで触れたことがなく、単に重い処理を実行するアプリケーションの応答性を改善したいだけなら、マルチスレッディングはお探しのものとは違うかもしれません。マルチスレッドアプリケーションはしばしば複雑であり、デバグは常に困難です。また、多くの場合マルチスレッディングは必要のないものです。単一スレッドアプリケーションで十分です。時間のかかるタスクを細かな部分に分割することができるなら、マルチスレッディングの代わりに Application.ProcessMessages を使うべきです。このメソッドは LCL に待機中のメッセージを全て処理させるものです。分割したタスクの一部分を実行したら Application.ProcessMessages を呼び、ユーザがアプリケーションを終了させたりどこかをクリックしたりしたかを確認し、あるいは進行状況表示を再描画したりします。その後タスクの次の部分を実行し、また Application.ProcessMessages を呼びます。

例: 大きなファイルの読み込みと処理。examples/multithreading/singlethreadingexample1.lpi 参照。

マルチスレッディングが必要なのは、次のような場合だけです -

  • 同期型のネットワーク通信のような、ブロッキングタイプの処理の場合(訳者注:同期型の場合タイムアウトするまで処理が戻ってこない可能性がありますから、例えば別スレッドでキャンセル待ち処理をするような使い方が考えられます)。
  • マルチプロセッサによる同時処理(SMP)の場合。
  • APIを呼び出さなければならないためにそれ以上細かく分割できないアルゴリズムやライブラリの呼び出し(訳者注:そのような分割できない長い処理を普通に行うと、ハングアップして固まったかのように見えてしまいますから、処理時間が長くかかる処理は別スレッドで処理して、描画やウィンドウの移動などの操作にはメインスレッドで反応するような使い方が考えられます)。

TThreadクラス

このサンプルは「examples/multithreading/」にあります。

マルチスレッドアプリケーションはTThreadクラスを使用すると簡単に作成できます。このクラスを用いると追加のスレッド(メインスレッドと並列に実行する)を作ることができます。

そのためには通常、Create コンストラクタと、Execute メソッドの二つのメソッドをオーバーライドしなければなりません。

コンストラクタは、あなたが走らせるスレッドを準備する際に使います。あなたは変数もしくはプロパティに適切な初期値を納める必要があります。TThread のオリジナルのコンストラクタは Suspended というパラメータを必要とします。名前からもおわかりのように、Suspended が False の場合、スレッドは作成後直ちに実行されます。逆に Suspended が True の場合、スレッドは停止状態で作成されます。この場合、スレッドを走らせるには Resume メソッドを呼びます。

FPCのバージョンが2.0.1またはそれ以降の場合、TThread.Create は StackSize という暗黙のパラメータを持っています。あなたは必要ならば自分の作るスレッドのデフォルトスタックサイズを変更することが可能です。 例えばスレッド内で深い再帰呼び出しを行う際には重宝するでしょう。あなたがスタックサイズを指定しなければ、OSによる規定のスタックサイズが使用されます。

スレッドで実行したいコードは、Execute メソッドをオーヴァーライドして、その中に書きます。

TThread クラスは一つの重要なプロパティを持っています。それは:

Terminated : boolean;

です。

Terminated のデフォルト値は False です。通常の場合、スレッドはループを持っていますが、Terminated が True になったら、そのループから脱出しなければなりません。したがってループのサイクル毎に、Terminatedが True になっていないかチェックする必要があります。もし True になっていたなら、速やかに必要なクリーンアップを行い、Execute メソッドを終了させなければなりません。

ですから、Terminated メソッドを呼び出してもデフォルト状態では何も起きないことを、しっかり覚えておいてください。Execute メソッド自体が自分自身を終了するように明示的に実装しなければなりません。

先に説明したように、スレッドは可視コンポーネントを操作すべきではありません。なにかをユーザーに表示するためにはメインスレッドで行います。これを行うために TThread には Synchronize というメソッドがあります。このメソッドは引数として、引数をもたない一つのメソッドをとります。たとえばMyMethodというメソッドをスレッドから実行するには、Synchronixe(@MyMethod)として呼び出します。この時スレッドの実行は一時的に停止し、メインスレッドから当該メソッドが実行され、その後スレッド実行が再開されます。このような仕組みのおかげで、MyMethodはコンテクスト(文脈)を持たずに実行されます。つまり、mousedownイヴェントやpaintイヴェント実行中に割り込んで実行されるのではなく、それらのイヴェントの処理が終わってから実行されます。MyMethodが実行された後、休眠状態になっていたスレッドは再開し、次のメッセージが処理されます。

TThread が持つもう一つの重要なプロパティに、FreeOnTerminate があります。このプロパティを True にしておくと、スレッドオブジェクトはスレッドの実行(Executeメソッド)が停止した後に自動的に解放されます。さもなくば、アプリケーションは手動でオブジェクトを解放する必要があります。

例:

 Type
   TMyThread = class(TThread)
   private
     fStatusText : string;
     procedure ShowStatus;
   protected
     procedure Execute; override;
   public
     Constructor Create(CreateSuspended : boolean);
   end;
 constructor TMyThread.Create(CreateSuspended : boolean);
 begin
   FreeOnTerminate := True;
   inherited Create(CreateSuspended);
 end;
 procedure TMyThread.ShowStatus;
 begin
   Form1.Caption := fStatusText;
 end;

 procedure TMyThread.Execute;
 var
   newStatus : string;
 begin
   fStatusText := 'Starting...';
   Synchronize(@Showstatus);
   fStatusText := 'Running...';
   while (not Terminated) and ([any condition required]) do
     begin
       ...
       [here goes the code of the main thread loop]
       ...
       if NewStatus <> fStatusText then
         begin
           fStatusText := newStatus;
           Synchronize(@Showstatus);
         end;
     end;
 end;

アプリケーション側では,

 var
   MyThread : TMyThread;
 begin
   MyThread := TMyThread.Create(True); // This way it doesn't start automatically
   ...
   [Here the code initialises anything required before the threads starts executing]
   ...
   MyThread.Resume;
 end;


もっと柔軟な制御を望むなら、スレッドに対しイヴェントを発行することができ - そうすると、sychronizeされたメソッドは特定のフォームやクラスに縛られなくなります - そのイヴェントに対するリスナーを設定することができます。次の例で、TMyThread.ShowStatusはメインスレッドで実行されるので、全てのGUI要素に触ることができます:


 Type
   TShowStatusEvent = procedure(Status: String) of Object;
   TMyThread = class(TThread)
   private
     fStatusText : string;
     FOnShowStatus: TShowStatusEvent;
     procedure ShowStatus;
   protected
     procedure Execute; override;
   public
     Constructor Create(CreateSuspended : boolean);
     property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
   end;
 constructor TMyThread.Create(CreateSuspended : boolean);
 begin
   FreeOnTerminate := True;
   inherited Create(CreateSuspended);
 end;
 procedure TMyThread.ShowStatus;
 // this method is executed by the mainthread and can therefore access all GUI elements.
 begin
   if Assigned(FOnShowStatus) then
   begin
     FOnShowStatus(fStatusText);
   end;
 end;
 procedure TMyThread.Execute;
 var
   newStatus : string;
 begin
   fStatusText := 'TMyThread Starting...';
   Synchronize(@Showstatus);
   fStatusText := 'TMyThread Running...';
   while (not Terminated) and ([any condition required]) do
     begin
       ...
       [here goes the code of the main thread loop]
       ...
       if NewStatus <> fStatusText then
         begin
           fStatusText := newStatus;
           Synchronize(@Showstatus);
         end;
     end;
 end;

アプリケーション側では,

 Type
   TForm1 = class(TForm)
     Button1: TButton;
     Label1: TLabel;
     procedure FormCreate(Sender: TObject);
     procedure FormDestroy(Sender: TObject);
   private
     { private declarations }
     MyThread: TMyThread; 
     procedure ShowStatus(Status: string);
   public
     { public declarations }
   end;
 procedure TForm1.FormCreate(Sender: TObject);
 begin
   inherited;
   MyThread := TMyThread.Create(true);
   MyThread.OnShowStatus := @ShowStatus;
 end;
 procedure TForm1.FormDestroy(Sender: TObject);
 begin
   MyThread.Terminate;
   MyThread.Free;
   inherited;
 end;
 procedure TForm1.Button1Click(Sender: TObject);
 begin
  MyThread.Resume;
 end;
 procedure TForm1.ShowStatus(Status: string);
 begin
   Label1.Caption := Status;
 end.

特別に注意すべきこと

Windowsで-Ctスイッチ(スタックチェック)をつかうスレッドには頭の痛い問題があります。理由は定かではありませんが、デフォルトのスタックサイズを使う場合、スタックチェックが TTread.Create をトリガーしてしまうことがあります。現在の運用上の回避策としては -Ctスイッチを使わないことです。これがメインスレッドで例外をひきおこすことは全くなく、新しく生成されたスレッドで起こります。そのためスレッドが絶対開始できないように見えます。

スレッド生成時に発生するその他の例外や、この問題をチェックする良いコードを示します。

    MyThread:=TThread.Create(False);
    if Assigned(MyThread.FatalException) then
      raise MyThread.FatalException;

このコードはスレッド生成時に発生するどんな例外でも、メインのスレッドでraiseするようにします。

マルチスレッドアプリケーションで必要なユニット

Windowsでは特に気をつけることはないのですが、LinuxやMaxOSX,FreeBSDでは、かならずプロジェクトのユニット(つまり、プログラムユニット、.lpr)で、cthreadsユニットをusesする必要があります。

Lazarusアプリケーションのコードでは次のようになるでしょう。

 program MyMultiThreadedProgram;
 {$H+}
 uses
 {$ifdef unix}
   cthreads,
 {$endif}
   Interfaces, // this includes the LCL widgetset
   Forms
   { add your units here },

これを怠ると、起動時に次のエラーが出ます:

 This binary has no thread support compiled in.
 Recompile the application with a thread-driver in the program uses clause before other units using thread.

(このバイナリには、スレッドをサポートするコードが含まれていません。uses節の中で、スレッドを利用するユニットより前にスレッドドライバを加えて再コンパイルしてください)

SMP(対称型マルチプロセッサCPU)のサポート

良いニュースです。この用法でマルチスレッドでアプリケーションとして正しく動作すれば、それはSMPで並列動作が有効になります。

Lazarusでのマルチスレッドのデバッグ

Lazarusでのマルチスレッドのデバッグは、まだ完全に機能しません。

デバグ出力

シングルスレッドアプリケーションでは、単にコンソール/ターミナル/その他に一行一行出力すれば、その順序に書き込まれます。マルチスレッドアプリケーションではそうは問屋が卸しません。二つのスレッドA,Bがあり、まずAが、次いでBが行を出力したとします。しかし、必ずしもその順序で行が並ぶわけではありません。さらには、行の途中で別のスレッドの出力が割り込んでくることだってあり得ます。

LCLProcユニットに、スレッド毎にログを保存するための手続きがあります:

 procedure DbgOutThreadLog(const Msg: string); overload;
 procedure DebuglnThreadLog(const Msg: string); overload;
 procedure DebuglnThreadLog(Args: array of const); overload;
 procedure DebuglnThreadLog; overload;

例: writeln('Some text ',123); の代わりに、これを使います

 DebuglnThreadLog(['Some text ',123]);

こうすると、 'Some text 123' という行が Log<PID>.txt に追加されます。ここで <PID> はそのスレッドのプロセスIDです。

実行する前毎に、ログファイルを削除するとよいでしょう:

 rm -f Log* && ./project1

Linux

もしマルチスレッドアプリケーションをLinuxでデバッグしようとすると、Xサーバーがハングするといった大きな問題に直面するでしょう。

これはどうやって解決すべきか分かっていませんが、回避策としては、Xのインスタンスを次のように新しく作ります。

 X :1 &

これでOpenして、他のデスクトップへスイッチしたとき、(the one you are working with pressing CTRL+ALT+F7), 元のグラフィカルデスクトップへCTRL+ALT+F8で戻ることができます。 (もしこの組み合わせがうまくいかなかったらSlackwareでのCTRL+ALT+F2で)

もし、これができたら、Xが開始するデスクトップセッションを次のようにして作ります。

 gnome-session --display=:1 &

そしてLazarusでプロジェクトの run parameters dialogで"Use display"をチェックして、:1を入力します。

これでアプリケーションは2番目のXサーバーで動作するようになり、最初のXサーバーでデバッグが可能になりました。

この方法は、FPC2.0とLazarus 0.9.10で、OSはWindowsとLinuxでテストしました。


新しい X セッションを開始する代わりに、 Xnest を用いることもできます。Xnest は X セッションをウィンドウで開くものです。これを用いると、スレッドをデバグする間も X サーバがロックしません。また、複数のターミナルを行ったり来たりしながらデバグするよりも楽です。

Xnestを起動するには、次のようなコマンドをタイプします:

 Xnest :1 -ac

一つの X セッションが :1 で開き、アクセス制御がディセーブルされます。

ウィジェットセット

Win32、gtk、Carbon インタフェースはマルチスレッディングを完全にサポートしています。即ち TThread、クリティカル・セクション、Synchronize が動作します。

クリティカル・セクション

クリティカル・セクションはオブジェクトの一つで、ある部分のコードがある時点では一つのスレッドだけで実行されること(排他的に実行されること)を保証するものです。クリティカル・セクションは使用前に作成/初期化し、使用後は解放する必要があります。

通常の使い方です:

1) SyncObjs ユニットを追加します。

2) セクションを宣言します。そのセクションを実行する可能性のある全てのスレッドから見えるよう十分大域に宣言しなければなりません:

 MyCriticalSection: TRTLCriticalSection;

3) セクションを作成します:

 InitializeCriticalSection(MyCriticalSection);

4) いくつかのスレッドを走らせます。ここで排他的になにかをするには次のようにします:

 EnterCriticalSection(MyCriticalSection);
 try
   // 変数へのアクセス、ファイル出力、ネットワークパケットの送信など
 finally
   LeaveCriticalSection(MyCriticalSection);
 end;

5) 全てのスレッドが停止した後、セクションを解放します:

 DeleteCriticalSection(MyCriticalSection);

別の方法として、TCriticalSection オブジェクトも使えます。オプジェクトを作成するとセクションの作成と初期化を、Enter メソッドを実行すると EnterCriticalSection と同様の動作を、Leave メソッドを実行すると LeaveCriticalSection と同様の動作を、オブジェクトを破棄するとセクションの削除を行います。

For example: 5 threads incrementing a counter. See lazarus/examples/multithreading/criticalsectionexample1.lpi

注意: 上記の4つの手続きは RTL(FPCのランタイム)と LCL に一組づつ存在します。RTL のは SyncObjs ユニットにありますが、LCL のは LCLIntf ユニットと LCLType で定義されています。どちらの組も同様に用いることができます。アプリケーションの中で混用することもできますが、RTL の関数/手続きには RTL のクリティカルセクションを、LCL の関数/手続きには LCL のクリティカルセクションを使わなければなりません。

変数の共用

複数のスレッドが一つの変数を共用する場合、読み出すだけなら何も問題はありません。単に読めばいいのです。しかし、一つまたは複数のスレッドがその値を変更しようとするなら、複数のスレッドがその変数に同時にアクセスしないように注意しなければなりません。

例: 五つのスレッドが一つのカウンタをインクリメントします。

lazarus/examples/multithreading/criticalsectionexample1.lpi 参照。

別のスレッドの処理を待つ方法

スレッドAが別のスレッドBの結果を利用するなら、AはBの処理が終了するまで待つ必要があります。

重要: メインスレッドは決して他のスレッドの処理を待ってはいけません。その代わりに上記の Synchronize を用います。

参照すべき例: lazarus/examples/multithreading/waitforexample1.lpi

{ TThreadA }

procedure TThreadA.Execute;
begin
  Form1.ThreadB:=TThreadB.Create(false);
  // イヴェントの作成
  WaitForB:=RTLEventCreate;
  while not Application.Terminated do begin
    // Bの処理終了を永遠に待つ
    RtlEventWaitFor(WaitForB);
    writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
  end;
end;

{ TThreadB }

procedure TThreadB.Execute;
var
  i: Integer;
begin
  Counter:=0;
  while not Application.Terminated do begin
    // B実行中
    Sleep(1500);
    inc(Counter);
    // Aを起こす
    RtlEventSetEvent(Form1.ThreadA.WaitForB);
  end;
end;

Fork

When forking in a multithreaded application, be aware that any threads created and running BEFORE the fork (or fpFork) call, will NOT be running in the child process. As stated on the fork() man page, any threads that were running before the fork call, their state will be undefined.

So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.