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 参照。

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

  • ハンドルの把持。ネットワーク通信の際など。
  • マルチプロセッサによる同時処理
  • 分割不可能なアルゴリズム、ライブラリ呼び出し。

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がTrueであって、スレッドがループを持っている場合(これは普通です)、ループは終了されます(デフォルトではFalseです)。したがってどんなサイクルもTerminatedが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でテストしました。