Difference between revisions of "Multithreaded Application Tutorial/ja"

From Lazarus wiki
Jump to navigationJump to search
m (Fixed syntax highlighting; removed categories included in template)
 
(22 intermediate revisions by 5 users not shown)
Line 3: Line 3:
 
{{Japanese Menu}}
 
{{Japanese Menu}}
  
= 概要 =
+
== 概要 ==
このページでは Free Pascal と Lazarus を使ってマルチスレッドアプリケーションを書いたりデバッグしたりする方法について述べます。
+
このページでは Free Pascal と Lazarus を使ってマルチスレッドアプリケーションを記述したりデバッグしたりする方法について述べます。マルチスレッドアプリケーションは、同時作業させるために複数のスレッドを作成しているものを指します。もし、読者がマルチスレッドに初めて触れるのなら、是非マルチスレッドが本当に必要かどうかを「マルチスレッドが必要ですか?」節でお確かめください。そうすれば頭痛の種を激減することができるでしょう。
  
もし、読者がマルチスレッディングに初めて触れる読者は、是非マルチスレッディングが本当に必要かどうかを「マルチスレッディングが必要ですか」節でお確かめください。そうすれば頭痛の種を激減することができるでしょう。
+
スレッドのうち 1 つは、メインスレッドと呼ばれるものです。メインスレッドはアプリケーションが起動する際、オペレーティングシステムによって生成されます。メインスレッドはユーザインターフェイス、コンポーネントを更新する唯一のスレッドです。メインスレッドは'''かならず'''ただ一つである必要があり、そうでないとアプリケーションは暴走します。
  
マルチスレッドアプリケーションは、2つかそれ以上のスレッドを作成、同時実行して作業を行うものです。そのうち一つは、メインスレッドと呼ばれるものです。メインスレッドはアプリケーションが起動する際、オペレーティングシステムによって生成されます。メインスレッドはユーザーインターフェイス、コンポーネントを更新する唯一のスレッドです。メインスレッドは'''かならず'''ただ一つである必要があり、そうでないとアプリケーションは暴走します。
+
マルチスレッドプログラミングの基本的な考え方は、ユーザがメインスレッドを通して作業する傍ら、もう一つのスレッドを用いてある種の処理をバックグラウンドで行えるようにアプリケーションを組むという点です。
 +
 
 +
スレッドの他の使い方として、アプリケーションの応答速度を改善することが挙げられます。例えば、あなたがアプリケーションを作られたとして、ユーザがボタンを押してアプリケーションがすごく重たい処理を開始したとします。処理の間、スクリーンは凍りつき反応が無くなり、ユーザはアプリケーションが落ちたと思うでしょう。よくないことです。もし、その重たい処理を 2 つ目のスレッドの中で実行するようにすれば、アプリケーションの応答性は保たれ、まるでそんな処理は行っていないかのように振舞います。このような場合には、スレッドを開始する前にフォームの処理を開始するボタンを使用不可能にしておくと、ユーザがそれ以上の作業を要求することを無くすことができ、好都合です。
 +
 
 +
また、サーバアプリケーションを作る際に、同時に大量のクライアントに応答しなければならない場合にも使えます。
 +
 
 +
==マルチスレッドが必要ですか?==
 +
 
 +
もし読者がマルチスレッドにこれまで触れたことがなく、単に重い処理を実行するアプリケーションの応答性を改善したいだけなら、マルチスレッドはお探しのものとは違うかもしれません。マルチスレッドアプリケーションはしばしば複雑であり、デバッグは常に困難です。また、多くの場合マルチスレッドは必要のないものです。単一スレッドアプリケーションで十分です。時間のかかるタスクを細かな部分に分割することができるなら、マルチスレッドの代わりに '''Application.ProcessMessages''' を使うべきです。このメソッドを使用すると、LCL に待機中のメッセージをすべて処理させたのち制御が戻ってきます。分割したタスクの一部分を実行したら Application.ProcessMessages を呼び、ユーザがアプリケーションを終了させたりどこかをクリックしたりしたかを確認し、あるいは進行状況表示を再描画したりします。その後タスクの次の部分を実行し、また Application.ProcessMessages を呼びます。
 +
 
 +
例:大きなファイルの読み込みと処理<br>
 +
examples/multithreading/singlethreadingexample1.lpi
 +
 
 +
マルチスレッドが必要なのは、次のような場合だけです。
 +
* 同期型のネットワーク通信のような、ブロッキングタイプの処理の場合(訳注:同期型の場合タイムアウトするまで処理が戻ってこない可能性がありますから、例えば別スレッドでキャンセル待ち処理をするような使い方が考えられます)。
 +
* マルチプロセッサによる同時処理(SMP)の場合。
 +
* API を呼び出さなければならないためにそれ以上細かく分割できないアルゴリズムやライブラリの呼び出し(訳注:分割できない長い処理を普通に行うとハングアップして固まったかのように見えてしまいますから、時間がかかる処理は別スレッドで行い描画やウィンドウの移動などの操作にはメインスレッドで反応するような使い方が考えられます)。
  
マルチスレッドプログラミングの基本的な考え方は、ユーザがメインスレッドを通して作業する傍ら、もう一つのスレッドを用いてある種の処理をバックグラウンドで行えるようにアプリケーションを組むという点です。
+
マルチプロセッサによる同時処理で処理速度を上げるためにマルチスレッドを使いたいのであれば、プログラムが 1 つの CPU コアのリソースを 100% フルに使い切っているか確認してください(例えば、プログラムでファイルの書き込みなど入出力の処理が多く占めている場合、時間がかかる作業ではありますが CPU には負荷はかかっていません。このようなケースでマルチスレッドを使ってもプログラムは速くならないでしょう。)。また、最適化レベルが最大の 3 なっているかも確認してください。最適化レベルを 1 から 3 に切り替えたら、プログラムが約 5 倍速くなったことがありました。
  
スレッドの他の使いかたとして、アプリケーションの応答速度を改善することが挙げられます。例えばあなたがアプリケーションをつくって、ユーザーがボタンを押して、アプリケーションが処理を開始したとします(すごく重たい処理です)。処理の間、スクリーンは凍りつき反応が無くなり、ユーザーはアプリケーションが落ちたと思うでしょう。よくないことです。もし、その重たい処理を2つ目のスレッドの中で実行するようにすれば、アプリケーションの応答性は保たれ、まるでそんな処理は行っていないかのように振舞います。このような場合には、スレッドを開始する前にフォームの処理を開始するボタンを使用不可能にしておくと、ユーザーがそれ以上の仕事を要求することを無くすことができ、好都合です。
+
==マルチスレッドアプリケーションで必要となるユニット==
 +
Windows では動作させるのに必要なユニットは特にありません。Linux、Mac OS X、FreeBSD では cthreads ユニットが必要で、プロジェクトユニット(プログラムソース、通常は .lpr ファイル)の一番最初に''必ず''置かなくてなりません!
  
また、サーバアプリケーションを作る際に、同時に大量のクライアントに返答しなければならない場合にも使えます。
+
Lazarus アプリケーションコードでは以下のようになるでしょう。(訳注:実際に見れば分かりますが、cthreads は自動生成された時点で既に UseCThreads スイッチ付きで組み込まれています。)
  
= マルチスレッディングが必要ですか =
+
<syntaxhighlight lang=pascal>
もし読者がマルチスレッディングにこれまで触れたことがなく、単に重い処理を実行するアプリケーションの応答性を改善したいだけなら、マルチスレッディングはお探しのものとは違うかもしれません。マルチスレッドアプリケーションはしばしば複雑であり、デバグは常に困難です。また、多くの場合マルチスレッディングは必要のないものです。単一スレッドアプリケーションで十分です。時間のかかるタスクを細かな部分に分割することができるなら、マルチスレッディングの代わりに<tt> Application.ProcessMessages </tt>を使うべきです。このメソッドは LCL に待機中のメッセージを全て処理させるものです。分割したタスクの一部分を実行したら Application.ProcessMessages を呼び、ユーザがアプリケーションを終了させたりどこかをクリックしたりしたかを確認し、あるいは進行状況表示を再描画したりします。その後タスクの次の部分を実行し、また Application.ProcessMessages を呼びます。
+
program MyMultiThreadedProgram;
 +
{$mode objfpc}{$H+}
 +
uses
 +
{$ifdef unix}
 +
  cthreads,
 +
  cmem, // the c memory manager is on some systems much faster for multi-threading
 +
{$endif}
 +
  Interfaces, // this includes the LCL widgetset
 +
  Forms
 +
  { you can add units here },
 +
</syntaxhighlight>
  
例: 大きなファイルの読み込みと処理。examples/multithreading/singlethreadingexample1.lpi 参照。
+
これを怠ると、起動時に以下のエラーが出ます。
 +
  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 節に置き、アプリケーションをコンパイルし直してください。
  
マルチスレッディングが必要なのは、次のような場合だけです -
+
{{Note|"mcount" が見つからないというようなリンカエラーが発生することがあります。これが発生するのは、マルチスレッドコードを含むユニットを使っていて、cthreads ユニットの追加もしくはスマートリンクの使用が必要な場合です。}}
* 同期型のネットワーク通信のような、ブロッキングタイプの処理の場合(訳者注:同期型の場合タイムアウトするまで処理が戻ってこない可能性がありますから、例えば別スレッドでキャンセル待ち処理をするような使い方が考えられます)。
+
{{Note|手続き SYSTEM_NOTHREADERROR 内でエラー "Project raised exception class 'RunError(232)'" が発生することがあります。これが発生するのは、コード内でスレッドが必要としており、cthreads ユニットの追加が必要な場合です。}}
* マルチプロセッサによる同時処理(SMP)の場合。
 
* APIを呼び出さなければならないためにそれ以上細かく分割できないアルゴリズムやライブラリの呼び出し(訳者注:そのような分割できない長い処理を普通に行うと、ハングアップして固まったかのように見えてしまいますから、処理時間が長くかかる処理は別スレッドで処理して、描画やウィンドウの移動などの操作にはメインスレッドで反応するような使い方が考えられます)。
 
  
= TThreadクラス =
+
==TThread クラス==
  
このサンプルは「examples/multithreading/」にあります。
+
サンプルは「examples/multithreading/」にあります。
  
マルチスレッドアプリケーションはTThreadクラスを使用すると簡単に作成できます。このクラスを用いると追加のスレッド(メインスレッドと並列に実行する)を作ることができます。
+
マルチスレッドアプリケーションは TThread クラスを使用すると簡単に作成できます。このクラスを用いると追加のスレッド(メインスレッドと並列に実行する)を作ることができます。そのためには通常、Create コンストラクタと、Execute メソッドの 2 つのメソッドをオーバーライドしなければなりません。
  
そのためには通常、Create コンストラクタと、Execute メソッドの二つのメソッドをオーバーライドしなければなりません。
+
コンストラクタは、スレッドを準備する際に使います。必要となる変数ないしプロパティに初期値を設定します。基となる TThread のコンストラクタは Suspended という引数を必要とします。名前からもお分かりのように、Suspended が False の場合、スレッドは作成後直ちに実行されます。逆に Suspended が True の場合、スレッドは停止状態で作成されます。この場合、スレッドを実行するには Start メソッドを呼びます。
  
コンストラクタは、あなたが走らせるスレッドを準備する際に使います。あなたは変数もしくはプロパティに適切な初期値を納める必要があります。TThread のオリジナルのコンストラクタは Suspended というパラメータを必要とします。名前からもおわかりのように、Suspended が False の場合、スレッドは作成後直ちに実行されます。逆に Suspended が True の場合、スレッドは停止状態で作成されます。この場合、スレッドを走らせるには Resume メソッドを呼びます。
+
{{Note|メソッド Resume は [http://wiki.freepascal.org/User_Changes_2.4.4#TThread.Suspend_and_TThread.Resume_have_been_deprecated FPC 2.4.4 以降非推奨になりました]。Resume は Start に置き換えてください。}}
  
FPCのバージョンが2.0.1またはそれ以降の場合、TThread.Create は StackSize という暗黙のパラメータを持っています。あなたは必要ならば自分の作るスレッドのデフォルトスタックサイズを変更することが可能です。
+
FPC バージョン 2.0.1 以降の場合、TThread.Create は StackSize という暗黙のパラメータを持っています。必要であれば作成されたスレッドのデフォルトスタックサイズを変更することが可能です。例えばスレッド内で深い再帰呼び出しを行う際には重宝するでしょう。スタックサイズを指定しなかった場合は、OS による規定のスタックサイズが使用されます。
例えばスレッド内で深い再帰呼び出しを行う際には重宝するでしょう。あなたがスタックサイズを指定しなければ、OSによる規定のスタックサイズが使用されます。
 
  
スレッドで実行したいコードは、Execute メソッドをオーヴァーライドして、その中に書きます。
+
スレッドで実行したいコードは、Execute メソッドをオーバーライドして、その中に書きます。
  
TThread クラスは一つの重要なプロパティを持っています。それは:
+
TThread クラスは一つの重要なプロパティを持っています。それは、
 
  Terminated : boolean;
 
  Terminated : boolean;
 
です。
 
です。
  
Terminated のデフォルト値は False です。通常の場合、スレッドはループを持っていますが、Terminated が True になったら、そのループから脱出しなければなりません。したがってループのサイクル毎に、Terminatedが True になっていないかチェックする必要があります。もし True になっていたなら、速やかに必要なクリーンアップを行い、Execute メソッドを終了させなければなりません。
+
Terminated のデフォルト値は False です。通常の場合、スレッドはループを持っていますが、Terminated が True になったら、そのループから脱出しなければなりません。したがってループのサイクル毎に、Terminated が True になっていないかチェックする必要があります。もし True になっていたなら、速やかに必要な終了処理を行い、Execute メソッドを終了させなければなりません。ですから、Terminate メソッドを呼び出してもデフォルト状態では何も起きないことを、しっかり覚えておいてください。Execute メソッド自体が自分自身を終了するように明示的に実装しなければなりません。
 +
 
 +
先に説明したように、スレッドはビジュアルコンポーネントを操作すべきではありません。なにかをユーザに表示するためにはメインスレッドで行います。
  
ですから、Terminated メソッドを呼び出してもデフォルト状態では何も起きないことを、しっかり覚えておいてください。Execute メソッド自体が自分自身を終了するように明示的に実装しなければなりません。
+
これを行うために TThread には Synchronize というメソッドがあります。このメソッドは引数として、引数を持たないメソッドを 1 つとります。例えば、MyMethod というメソッドをスレッドから実行するには、Synchronize(@MyMethod) として呼び出します。この時スレッドの実行は一時的に停止し、メインスレッドから当該メソッドが実行され、その後スレッド実行が再開されます。
  
先に説明したように、スレッドは可視コンポーネントを操作すべきではありません。なにかをユーザーに表示するためにはメインスレッドで行います。これを行うために TThread には<tt> Synchronize </tt>というメソッドがあります。このメソッドは引数として、引数をもたない一つのメソッドをとります。たとえばMyMethodというメソッドをスレッドから実行するには、Synchronixe(@MyMethod)として呼び出します。この時スレッドの実行は一時的に停止し、メインスレッドから当該メソッドが実行され、その後スレッド実行が再開されます。このような仕組みのおかげで、MyMethodはコンテクスト(文脈)を持たずに実行されます。つまり、mousedownイヴェントやpaintイヴェント実行中に割り込んで実行されるのではなく、それらのイヴェントの処理が終わってから実行されます。MyMethodが実行された後、休眠状態になっていたスレッドは再開し、次のメッセージが処理されます。
+
Synchronize の正確な動作はプラットフォーム依存しますが、基本的には以下のようになります。
 +
* メッセージをメインメッセージキューに投げ、スレッドは休眠状態になります。
 +
* メインスレッドでメッセージが処理され、MyMethod が呼ばれます。MyMethod は(デバイスや描画用の)コンテクスト無しで実行されます。つまり、MouseDown イベントや Paint イベント実行中に割り込んで実行されるのではなく、これらのイベントは後で実行されます。
 +
* メインスレッドで MyMethod が実行された後、スレッドの休眠状態が解かれ、次のメッセージが処理されます。
 +
* スレッドの処理が再開されます。
  
TThread が持つもう一つの重要なプロパティに、FreeOnTerminate があります。このプロパティを True にしておくと、スレッドオブジェクトはスレッドの実行(Executeメソッド)が停止した後に自動的に解放されます。さもなくば、アプリケーションは手動でオブジェクトを解放する必要があります。
+
TThread が持つもう一つの重要なプロパティに FreeOnTerminate があります。このプロパティを True にしておくと、スレッドの実行(Execute メソッド)が停止した後にスレッドオブジェクトは自動的に解放されます。これを行わない場合は、アプリケーション側で手動でオブジェクトを解放する必要があります。<br>
 +
(訳注:以下の例では単に FreeOnTerminate = True しているだけなので、スレッドが終了した場合 MyThread 変数が示すアドレスに不正にアクセスできてしまいます。スレッド終了時に必ず呼ばれる TThread.OnTerminate で MyThread := nil するなどして適切な処置を行ってください。OnTerminate イベントも Synchronize を通して呼ばれています。)
  
例:
+
例:
  
<delphi>
+
<syntaxhighlight lang=pascal> Type
  Type
 
 
     TMyThread = class(TThread)
 
     TMyThread = class(TThread)
 
     private
 
     private
Line 74: Line 108:
  
 
   procedure TMyThread.ShowStatus;
 
   procedure TMyThread.ShowStatus;
   // このメソッドはメインスレッドで実行されるので GUI 要素にアクセスすることができます
+
   // このメソッドはメインスレッドで実行されるので、すべての GUI 要素にアクセス可能です。
 
   begin
 
   begin
 
     Form1.Caption := fStatusText;
 
     Form1.Caption := fStatusText;
Line 89: Line 123:
 
       begin
 
       begin
 
         ...
 
         ...
         [here goes the code of the main thread loop]
+
         [ここにスレッドループの本体コードを記述します]
 
         ...
 
         ...
 
         if NewStatus <> fStatusText then
 
         if NewStatus <> fStatusText then
Line 98: Line 132:
 
       end;
 
       end;
 
   end;
 
   end;
</delphi>
+
</syntaxhighlight>
 
 
アプリケーション側では,
 
 
 
<delphi>
 
  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;
 
</delphi>
 
 
 
(日本語訳注: FreeOnTerminate = True で使用した場合に厄介なのはスレッドを制御するのが非常に困難になることです。 FreeOnTerminate = False のままで使用して、スレッドをメインスレッドで確実に制御するやり方のほうがより実用的でバグもでにくいのでお勧めです。スレッドが止まってもメモリが解放されない点がデメリットですが、工夫次第ではたいしたデメリットにはなりません。->例2)
 
 
 
例2:
 
 
 
<delphi>
 
  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 := False;
 
    inherited Create(CreateSuspended);
 
  end;
 
  
  procedure TMyThread.ShowStatus;
+
アプリケーション側では、
  // このメソッドはメインスレッドで実行されるので GUI 要素にアクセスすることができます。
 
  // また、共有変数への書き込みもこのメソッドでやればうまく動作します。
 
  begin
 
    Form1.Caption := fStatusText;
 
  end;
 
 
  procedure TMyThread.Execute;
 
  var
 
    newStatus : string;
 
  begin
 
    try
 
      fStatusText := 'TMyThread Starting...';
 
      Synchronize(@Showstatus);
 
      fStatusText := 'TMyThread Running...';
 
      while (not Terminated) and ([他のループ終了条件]) do
 
        begin
 
          ...
 
          [here goes the code of the main thread loop]
 
          ...
 
          if NewStatus <> fStatusText then
 
            begin
 
              fStatusText := newStatus;
 
              Synchronize(@Showstatus);
 
            end;
 
        end;
 
    finally
 
      fStatusText := ''; // メモリを節約するため空に
 
    end;
 
  end;
 
</delphi>
 
 
 
アプリケーション側では,
 
  
<delphi>
+
<syntaxhighlight lang=pascal>
 
   var
 
   var
 
     MyThread : TMyThread;
 
     MyThread : TMyThread;
 
   begin
 
   begin
     MyThread := TMyThread.Create(True); // This way it doesn't start automatically
+
     MyThread := TMyThread.Create(True); // このようにすると自動的に開始しません
    ...
 
    [Here the code initialises anything required before the threads starts executing]
 
    ...
 
    MyThread.Resume;
 
    ...
 
    // スレッドの再スタートが必要になる場合などでのスレッドの制御方法
 
    if Assigned(MyThread) then
 
    begin
 
      MyThread.Free; // 内部で Terminate メソッドがコールされますので確実に止まります
 
      MyThread := nil; // スレッドが止まっている印として一応
 
    end;
 
 
     ...
 
     ...
     MyThread := TMyThread.Create(True); // 再スタートの準備
+
     [ここにスレッドを実行する前に必要な初期化コードを記述します]
 
     ...
 
     ...
     MyThread.Resume; // 再スタート
+
     MyThread.Start;
   end;
+
   end;</syntaxhighlight>
</delphi>
 
  
もっと柔軟な制御を望むなら、スレッドに対しイヴェントを発行することができ - そうすると、sychronizeされたメソッドは特定のフォームやクラスに縛られなくなります - そのイヴェントに対するリスナーを設定することができます。次の例で、TMyThread.ShowStatusはメインスレッドで実行されるので、全てのGUI要素に触ることができます:
+
もっと柔軟な制御を望むなら、スレッドに対しイベントを作成します。こうすることで、イベントに対してイベントハンドラで設定できるようになり、Synchronize されたメソッドは特定のフォームやクラスに縛られなくなります。以下が例です。
  
<delphi>
+
<syntaxhighlight lang=pascal> Type
  Type
 
 
     TShowStatusEvent = procedure(Status: String) of Object;
 
     TShowStatusEvent = procedure(Status: String) of Object;
  
Line 218: Line 171:
  
 
   procedure TMyThread.ShowStatus;
 
   procedure TMyThread.ShowStatus;
   // this method is executed by the mainthread and can therefore access all GUI elements.
+
   // このメソッドはメインスレッドで実行されるので、すべての GUI 要素にアクセス可能です。
 
   begin
 
   begin
 
     if Assigned(FOnShowStatus) then
 
     if Assigned(FOnShowStatus) then
Line 233: Line 186:
 
     Synchronize(@Showstatus);
 
     Synchronize(@Showstatus);
 
     fStatusText := 'TMyThread Running...';
 
     fStatusText := 'TMyThread Running...';
     while (not Terminated) and ([any condition required]) do
+
     while (not Terminated) and ([他のループ終了条件]) do
 
       begin
 
       begin
 
         ...
 
         ...
         [here goes the code of the main thread loop]
+
         [ここにスレッドループの本体コードを記述します]
 
         ...
 
         ...
 
         if NewStatus <> fStatusText then
 
         if NewStatus <> fStatusText then
Line 245: Line 198:
 
       end;
 
       end;
 
   end;
 
   end;
</delphi>
+
</syntaxhighlight>
  
アプリケーション側では,
+
アプリケーション側では、
  
<delphi>
+
<syntaxhighlight lang=pascal> Type
  Type
 
 
     TForm1 = class(TForm)
 
     TForm1 = class(TForm)
 
       Button1: TButton;
 
       Button1: TButton;
Line 274: Line 226:
 
   begin
 
   begin
 
     MyThread.Terminate;
 
     MyThread.Terminate;
     MyThread.Free;
+
 
 +
     // FreeOnTerminate が true であれば、以下の記述は必要ありません。
 +
    // MyThread.Free;
 
     inherited;
 
     inherited;
 
   end;
 
   end;
Line 280: Line 234:
 
   procedure TForm1.Button1Click(Sender: TObject);
 
   procedure TForm1.Button1Click(Sender: TObject);
 
   begin
 
   begin
   MyThread.Resume;
+
   MyThread.Start;
 
   end;
 
   end;
  
Line 286: Line 240:
 
   begin
 
   begin
 
     Label1.Caption := Status;
 
     Label1.Caption := Status;
   end;
+
   end;</syntaxhighlight>
</delphi>
+
 
 +
==特に注意すべきこと==
 +
 
 +
=== Windows におけるスタックチェック ===
  
= 特別に注意すべきこと =
+
Windows -Ct スイッチ(スタックチェック)を使うスレッドには頭の痛い問題があります。理由は定かではありませんが、デフォルトのスタックサイズを使う場合、スタックチェックが TTread.Create 内で問題を引き起こすことがあります。現在の運用上の回避策としては -Ct スイッチを使わないことです。一連の現象がメインスレッドで例外を引き起こすことは全くなく、新しく生成されたスレッド内で起こります。そのためスレッドが絶対開始できないように見えます。
== Windows におけるスタックチェック ==
 
Windowsで-Ctスイッチ(スタックチェック)をつかうスレッドには頭の痛い問題があります。理由は定かではありませんが、デフォルトのスタックサイズを使う場合、スタックチェックが TTread.Create をトリガーしてしまうことがあります。現在の運用上の回避策としては -Ctスイッチを使わないことです。これがメインスレッドで例外をひきおこすことは全くなく、新しく生成されたスレッドで起こります。そのためスレッドが絶対開始できないように見えます。
 
  
 
スレッド生成時に発生するその他の例外や、この問題をチェックする良いコードを示します。
 
スレッド生成時に発生するその他の例外や、この問題をチェックする良いコードを示します。
  
    MyThread:=TThread.Create(False);
+
<syntaxhighlight lang=pascal>MyThread := TThread.Create(False);
    if Assigned(MyThread.FatalException) then
+
if Assigned(MyThread.FatalException) then
      raise MyThread.FatalException;
+
  raise MyThread.FatalException;</syntaxhighlight>
  
このコードはスレッド生成時に発生するどんな例外でも、メインのスレッドでraiseするようにします。
+
このコードは、スレッド生成時に発生するどんな例外でも、メインスレッドで生成させます。
  
= マルチスレッドアプリケーションで必要なユニット =
+
===パッケージでのマルチスレッド===
Windowsでは特に気をつけることはないのですが、LinuxやMaxOSX,FreeBSDでは、かならずプロジェクトのユニット(つまり、プログラムユニット、.lpr)で、cthreadsユニットをusesする必要があります。
+
マルチスレッドを使用しているパッケージには、'''-dUseCThreads''' フラグをカスタム使用オプションに追加してください。パッケージエディタを開いて、オプション → 使用 → カスタム、そこへ ''-dUseCThreads'' を追加します。これによって、このパッケージを使用している IDE を含めたすべてのプロジェクトおよびパッケージでこのフラグが定義されます。IDE および IDE によって作成されたアプリケーションには、.lpr ファイル内に以下のコードが組み込まれています。(訳注:cmem は自動生成で組み込まれてはいません(Lazarus 1.0.12/1.2RC1 で確認)。)
 +
<syntaxhighlight lang=pascal>
 +
uses
 +
  {$IFDEF UNIX}{$IFDEF UseCThreads}
 +
  cthreads,
 +
  cmem, // the c memory manager is on some systems much faster for multi-threading
 +
  {$ENDIF}{$ENDIF}
 +
</syntaxhighlight>
  
Lazarusアプリケーションのコードでは次のようになるでしょう。
+
===Heaptrc===
  
  program MyMultiThreadedProgram;
+
''cmem'' ユニットを使った上で -gh スイッチを使うことはできません。-gh スイッチは、(メモリーリークを調べるために)ヒープマネージャーを拡張する heaptrc ユニットを使用するものです。ですから(リークチェックしたいのであれば)、'''cmem''' ユニットを使ってから '''その後に''' '''heaptrc''' ユニットを置くようにする必要があります。(訳注:heaptrc は cmem を考慮するように作られているけど、cmem はそうではないからでしょう。)
  {$H+}
 
  uses
 
  {$ifdef unix}
 
    cthreads,
 
  {$endif}
 
    Interfaces, // this includes the LCL widgetset
 
    Forms
 
    { add your units here },
 
  
これを怠ると、起動時に次のエラーが出ます:
+
<syntaxhighlight lang=pascal>uses
   This binary has no thread support compiled in.
+
   {$IFDEF UNIX}{$IFDEF UseCThreads}
   Recompile the application with a thread-driver in the program uses clause before other units using thread.
+
   cthreads,
 +
  cmem, // the c memory manager is on some systems much faster for multi-threading
 +
  {$ENDIF}{$ENDIF}
 +
  heaptrc,</syntaxhighlight>
  
(このバイナリには、スレッドをサポートするコードが含まれていません。uses節の中で、スレッドを利用するユニットより前にスレッドドライバを加えて再コンパイルしてください)
+
==SMP(対称型マルチプロセッシング)のサポート==
 +
難しく考える必要はありません。このページで紹介されているマルチスレッドのやり方でアプリケーションを適切な動作をさせればいいのです。SMP は常に有効になっていますから。
  
= SMP(対称型マルチプロセッサCPU)のサポート =
+
==Lazarus でのマルチスレッドアプリケーションのデバッグ==
良いニュースです。この用法でマルチスレッドでアプリケーションとして正しく動作すれば、それはSMPで並列動作が有効になります。
+
LazarusでのデバグにはGDBが必要です。Lazarusのデバグ環境はどんどんフル装備かつ安定になっています。しかしながら Linux の一部のディストリビューションには問題をかかえたものもあります。
 +
(* 訳注: MacOSX 10.8以降の場合、Xcode から GDB が失われているため、ユーザが自力で GDB をインストールし、デバグのための特権を与える必要があります *)
  
= Lazarusでのマルチスレッドのデバッグ =
+
===デバッグ出力===
Lazarusでのマルチスレッドのデバッグは、まだ完全に機能しません。
+
単一スレッドアプリケーションでは、単純にコンソールでもターミナルでも好きなものに出力すれば、出力した順に行が並びます。マルチスレッドアプリケーションでは事態はもっと複雑です。スレッドAがスレッドBより先に一行出力したからといって、その順に書き込まれるとは限りません。あるスレッドが自分の出力を書いているときに、たまたま他のスレッドが行を書き込んでいるかも知れないのです。Linux では(多分)適切なDebugLn() 出力を得られるでしょう。Win32 ではDegubLn() をメインスレッドの外で使っていることによる例外が発生するかもしれません(おそらく DiskFull 例外)。頭痛を軽減するには、下記の DebugLnThreadLog() をお使いください。
  
== デバグ出力 ==
+
The LCLProc ユニットには、各スレッドが自身のログファイルを書くための手続き/関数がいくつかあります:
シングルスレッドアプリケーションでは、単にコンソール/ターミナル/その他に一行一行出力すれば、その順序に書き込まれます。マルチスレッドアプリケーションではそうは問屋が卸しません。二つのスレッドA,Bがあり、まずAが、次いでBが行を出力したとします。しかし、必ずしもその順序で行が並ぶわけではありません。さらには、行の途中で別のスレッドの出力が割り込んでくることだってあり得ます。
 
  
LCLProcユニットに、スレッド毎にログを保存するための手続きがあります:
+
<syntaxhighlight lang=pascal>  procedure DbgOutThreadLog(const Msg: string); overload;
  procedure DbgOutThreadLog(const Msg: string); overload;
 
 
   procedure DebuglnThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(Args: array of const); overload;
 
   procedure DebuglnThreadLog(Args: array of const); overload;
   procedure DebuglnThreadLog; overload;
+
   procedure DebuglnThreadLog; overload;</syntaxhighlight>
  
 
例:
 
例:
Line 341: Line 298:
 
   DebuglnThreadLog(['Some text ',123]);
 
   DebuglnThreadLog(['Some text ',123]);
  
こうすると、 'Some text 123' という行が '''Log<PID>.txt''' に追加されます。ここで <PID> はそのスレッドのプロセスIDです。
+
こうすると、 'Some text 123' という行が '''Log<PID>.txt''' に追加されます。ここで <PID> はそのスレッドのプロセス ID です。
  
 
実行する前毎に、ログファイルを削除するとよいでしょう:
 
実行する前毎に、ログファイルを削除するとよいでしょう:
 
   rm -f Log* && ./project1
 
   rm -f Log* && ./project1
  
== Linux ==
+
=== Linux ===
もしマルチスレッドアプリケーションをLinuxでデバッグしようとすると、Xサーバーがハングするといった大きな問題に直面するでしょう。
+
Linux でマルチスレッドアプリケーションをデバグする場合、一つの大きな問題があります: Xサーバの Desktop Manager がハングするかもしれません。例えば、デバグ中のアプリケーションがマウスやキーボードを捕獲し、そこで gdb がアプリケーションを一時停止させ、Xサーバがアプリケーションを待ってしまうとこうなります。こうなってしまったら、他のコンピュータからログインするか、CTRL+ALT+F3 でそのセッションから抜け出すかして、gdb を kill してください。あるいは、ウィンドウマネジャを再起動することもできます: 次のように入力してください sudo /etc/init.d/gdm restart。これで Desktop Manager が再起動し、デスクトップに戻れます。
 +
 
 +
この問題は、gdb がデバグ中のプログラムのどこで実行を止めるかに依存するので、ちょっとしたトリックが有効な場合があります:  Ubuntu x64 では、プロジェクトオプション → コンパイラオプション → リンク → デバッグ:外部の gdb デバッグシンボルファイルを使用 (-Xg) をチェックします。
  
これはどうやって解決すべきか分かっていませんが、回避策としては、Xのインスタンスを次のように新しく作ります。
+
別のXデスクトップを開く手もあります。IDEとgdbを動かすデスクトップと、アプリケーションを動かすデスクトップを分けるのです。そうすれば固まるのはテスト用のデスクトップだけです。Xの別インスタンスを作るには:
  
 
   X :1 &
 
   X :1 &
  
これでOpenして、他のデスクトップへスイッチしたとき、(the one you are working with pressing CTRL+ALT+F7), 元のグラフィカルデスクトップへCTRL+ALT+F8で戻ることができます。
+
とします。元のデスクトップに戻るには CTRL+ALT+F7を、再び新しいデスクトップに行くには CTRL+ALT+F8 を使います (このキーコンビネーションがうまく行かなかったら、CTRL+ALT+F2 を試してください...  [http://www.slackware.com Slackware]で有効でした)。
(もしこの組み合わせがうまくいかなかったらSlackwareでのCTRL+ALT+F2で)
 
  
もし、これができたら、Xが開始するデスクトップセッションを次のようにして作ります。
+
これで、X上のデスクトップセッションを始められます:
  
 
   gnome-session --display=:1 &
 
   gnome-session --display=:1 &
Line 364: Line 322:
 
これでアプリケーションは2番目のXサーバーで動作するようになり、最初のXサーバーでデバッグが可能になりました。
 
これでアプリケーションは2番目のXサーバーで動作するようになり、最初のXサーバーでデバッグが可能になりました。
  
この方法は、FPC2.0とLazarus 0.9.10で、OSはWindowsとLinuxでテストしました。
+
この方法は、FPC2.0 と Lazarus 0.9.10 で、OS は Windows と Linux でテストしました。
 +
 
  
 
----
 
----
  
新しい X セッションを開始する代わりに、 [http://en.wikipedia.org/wiki/Xnest Xnest] を用いることもできます。Xnest は X セッションをウィンドウで開くものです。これを用いると、スレッドをデバグする間も X サーバがロックしません。また、複数のターミナルを行ったり来たりしながらデバグするよりも楽です。
+
新しい X セッションを開始する代わりに、 [http://en.wikipedia.org/wiki/Xnest Xnest] を用いることもできます。Xnest は X セッションをウィンドウで開くものです。これを用いると、スレッドをデバッグする間も X サーバがロックしません。また、複数のターミナルを行ったり来たりしながらデバグするよりも楽です。
  
 
Xnestを起動するには、次のようなコマンドをタイプします:
 
Xnestを起動するには、次のようなコマンドをタイプします:
Line 376: Line 335:
 
一つの X セッションが :1 で開き、アクセス制御がディセーブルされます。
 
一つの X セッションが :1 で開き、アクセス制御がディセーブルされます。
  
= ウィジェットセット =
+
==Lazarus ウィジェットセットインターフェース==
 +
win32、gtk、carbon インターフェースはマルチスレッドをサポートしています。それは TThread、クリティカルセクション、同期動作のことを指しています。しかしながら、これらはスレッドセーフではないのです。つまり、ある瞬間に同時に LCL にアクセスできるスレッドは 1 つに限られているということです。また、メインスレッドが他のスレッドに対して待機するようなことは通常はなく、メインスレッドだけが LCL へのアクセスを許可されているのです。TControl、アプリケーション、LCL ウィジェットの各ハンドルを使って動作させられるのはメインスレッドだけなのです。その一方で、LCL にはスレッドセーフな関数もいくつかあります。例えば、FileUtil ユニット内のほとんどの関数はスレッドセーフです。
 +
 
 +
===スレッド間のやり取りに SendMessage/PostMessage を使用する===
 +
 
 +
1 つのアプリケーションにおいて LCL API を呼び出すのはただ 1 つであるべきで、通常はメインスレッドがそれを担っています。いくつかの間接的手段を通すことで、他のスレッドでも LCL の使用が可能になります。SendMessage や PostMessage の使用もそのような手段の一つといえるでしょう。LCLIntf.SendMessage と LCLIntf.PostMessage は、メッセージをアプリケーションのメッセージプールに送り、ウィンドウを直接制御するものです。
 +
 
 +
これらのルーチンのドキュメントもお読みください。
 +
*[[doc:lcl/lclintf/sendmessage.html|SendMessage]]
 +
*[[doc:lcl/lclintf/postmessage.html|PostMessage]]
 +
 
 +
SendMessage と PostMessage の違いは、呼び出した側への制御の復帰方法です。SendMessage ではメッセージを送った先のウィンドウの処理が完了するまで制御が戻りませんが、PostMessage では即座に制御が戻ります。
 +
 
 +
以下は、別のスレッドよりメインスレッドへ LCL コントロールに表示させるテキストを送る方法の例です。
 +
 
 +
<syntaxhighlight lang=pascal>
 +
const
 +
  WM_GOT_ERROR          = LM_USER + 2004;
 +
  WM_VERBOSE            = LM_USER + 2005;
 +
 
 +
procedure VerboseLog(Msg: string);
 +
var
 +
  PError: PChar;
 +
begin
 +
  if MessageHandler = 0 then Exit;
 +
  PError := StrAlloc(Length(Msg)+1);
 +
  StrCopy(PError, PChar(Msg));
 +
  PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
 +
end;
 +
</syntaxhighlight>
 +
 
 +
こちらは、受け取ったメッセージの処理の例です。
 +
 
 +
<syntaxhighlight lang=pascal>
 +
const
 +
  WM_GOT_ERROR          = LM_USER + 2004;
 +
  WM_VERBOSE            = LM_USER + 2005;
  
Win32、gtk、Carbon インタフェースはマルチスレッディングを完全にサポートしています。即ち TThread、クリティカル・セクション、Synchronize が動作します。
+
type
 +
  { TformConsole }
  
= クリティカル・セクション =
+
  TformConsole = class(TForm)
 +
    DebugList: TListView;
 +
    // ...
 +
  private
 +
    procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
 +
  end;
  
''クリティカル・セクション''はオブジェクトの一つで、ある部分のコードがある時点では一つのスレッドだけで実行されること(排他的に実行されること)を保証するものです。クリティカル・セクションは使用前に作成/初期化し、使用後は解放する必要があります。
+
var
 +
  formConsole: TformConsole;
  
通常の使い方です:
+
implementation
  
1) SyncObjs ユニットを追加します。
+
....
  
2) セクションを宣言します。そのセクションを実行する可能性のある全てのスレッドから見えるよう十分大域に宣言しなければなりません:
+
{ TformConsole }
 +
 
 +
procedure TformConsole.HandleDebug(var Msg: TLMessage);
 +
var
 +
  Item: TListItem;
 +
  MsgStr: PChar;
 +
  MsgPasStr: string;
 +
begin
 +
  MsgStr := PChar(Msg.wparam);
 +
  MsgPasStr := StrPas(MsgStr);
 +
  Item := DebugList.Items.Add;
 +
  Item.Caption := TimeToStr(SysUtils.Now);
 +
  Item.SubItems.Add(MsgPasStr);
 +
  Item.MakeVisible(False);
 +
  //f/TrayControl.SetError(MsgPasStr);
 +
end;
 +
 
 +
end.
 +
</syntaxhighlight>
 +
 
 +
==クリティカルセクション==
 +
''クリティカルセクション''はオブジェクトの一つで、ある部分のコードがある時点では 1 つのスレッドだけで実行されること(排他的に実行されること)を保証するものです。クリティカルセクションは使用前に作成/初期化し、使用後は解放する必要があります。
 +
 
 +
以下はクリティカルセクションの一般的な使い方です。
 +
 
 +
セクションを宣言します(すべてのスレッドからアクセスできるようにグローバルにします)。
 
   MyCriticalSection: TRTLCriticalSection;
 
   MyCriticalSection: TRTLCriticalSection;
  
3) セクションを作成します:
+
セクションを作成します。
 
   InitializeCriticalSection(MyCriticalSection);
 
   InitializeCriticalSection(MyCriticalSection);
  
4) いくつかのスレッドを走らせます。ここで排他的になにかをするには次のようにします:
+
スレッドを実行します。排他的な処理は以下のようにします。
  EnterCriticalSection(MyCriticalSection);
+
<syntaxhighlight lang=pascal>
  try
+
EnterCriticalSection(MyCriticalSection);
    // 変数へのアクセス、ファイル出力、ネットワークパケットの送信など
+
try
  finally
+
  // 変数へのアクセス、ファイルの書き出し、ネットワークパケットの送信、など
    LeaveCriticalSection(MyCriticalSection);
+
finally
  end;
+
  LeaveCriticalSection(MyCriticalSection);
 +
end;</syntaxhighlight>
  
5) 全てのスレッドが停止した後、セクションを解放します:
+
すべてのスレッドが停止した後、セクションを解放します。
 
   DeleteCriticalSection(MyCriticalSection);
 
   DeleteCriticalSection(MyCriticalSection);
  
 
別の方法として、TCriticalSection オブジェクトも使えます。オプジェクトを作成するとセクションの作成と初期化を、Enter メソッドを実行すると EnterCriticalSection と同様の動作を、Leave メソッドを実行すると LeaveCriticalSection と同様の動作を、オブジェクトを破棄するとセクションの削除を行います。
 
別の方法として、TCriticalSection オブジェクトも使えます。オプジェクトを作成するとセクションの作成と初期化を、Enter メソッドを実行すると EnterCriticalSection と同様の動作を、Leave メソッドを実行すると LeaveCriticalSection と同様の動作を、オブジェクトを破棄するとセクションの削除を行います。
  
For example: 5 threads incrementing a counter.
+
例:5 つのスレッドが 1 つのカウンターに対して増加処理を行います。<br>
See lazarus/examples/multithreading/criticalsectionexample1.lpi
+
lazarus/examples/multithreading/criticalsectionexample1.lpi
 
 
'''注意:''' 上記の4つの手続きは RTL(FPCのランタイム)と LCL に一組づつ存在します。RTL のは SyncObjs ユニットにありますが、LCL のは LCLIntf ユニットと LCLType で定義されています。どちらの組も同様に用いることができます。アプリケーションの中で混用することもできますが、RTL の関数/手続きには RTL のクリティカルセクションを、LCL の関数/手続きには LCL のクリティカルセクションを使わなければなりません。
 
  
== 変数の共用 ==
+
'''注意:'''上述の 4 つの手続きは RTL(FPC のランタイム)と LCL に一組ずつ存在します。LCL のは LCLIntf と LCLType ユニットで定義されています(RTL のは System 系ユニット内)。どちらの組も同様に用いることができます。アプリケーションの中で混用することもできますが、RTL の関数/手続きには RTL のクリティカルセクションを、LCL の関数/手続きには LCL のクリティカルセクションを使わなければなりません。
  
複数のスレッドが一つの変数を共用する場合、読み出すだけなら何も問題はありません。単に読めばいいのです。しかし、一つまたは複数のスレッドがその値を変更しようとするなら、複数のスレッドがその変数に同時にアクセスしないように注意しなければなりません。
+
=== 変数の共用 ===
 +
複数のスレッドが 1 つの変数を共用する場合、読み出すだけなら何も問題はありません。単に読めばいいのです。しかし、1 つまたは複数のスレッドがその値を変更しようとするなら、複数のスレッドがその変数に同時にアクセスしないように注意しなければなりません。
  
例: 五つのスレッドが一つのカウンタをインクリメントします。
+
例:5 つのスレッドが 1 つのカウンターに対して増加処理を行います。<br>
lazarus/examples/multithreading/criticalsectionexample1.lpi 参照。
+
lazarus/examples/multithreading/criticalsectionexample1.lpi  
  
 
== 別のスレッドの処理を待つ方法 ==
 
== 別のスレッドの処理を待つ方法 ==
 +
スレッド A が別のスレッド B の結果を利用するなら、A は B の処理が終了するまで待つ必要があります。
  
スレッドAが別のスレッドBの結果を利用するなら、AはBの処理が終了するまで待つ必要があります。
+
'''重要:'''メインスレッドは決して他のスレッドの処理を待ってはいけません。その代わりに上述の Synchronize を用います。
 
 
'''重要:''' メインスレッドは決して他のスレッドの処理を待ってはいけません。その代わりに上記の Synchronize を用います。
 
  
参照すべき例: lazarus/examples/multithreading/waitforexample1.lpi
+
一例:lazarus/examples/multithreading/waitforexample1.lpi
  
<pre>
+
<syntaxhighlight lang=pascal>{ TThreadA }
{ TThreadA }
 
  
 
procedure TThreadA.Execute;
 
procedure TThreadA.Execute;
 
begin
 
begin
 
   Form1.ThreadB:=TThreadB.Create(false);
 
   Form1.ThreadB:=TThreadB.Create(false);
   // イヴェントの作成
+
   // イベント作成
 
   WaitForB:=RTLEventCreate;
 
   WaitForB:=RTLEventCreate;
 
   while not Application.Terminated do begin
 
   while not Application.Terminated do begin
     // Bの処理終了を永遠に待つ
+
     // B が A を起動させるまで待機し続けます
 
     RtlEventWaitFor(WaitForB);
 
     RtlEventWaitFor(WaitForB);
 
     writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
 
     writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
Line 450: Line 475:
 
   Counter:=0;
 
   Counter:=0;
 
   while not Application.Terminated do begin
 
   while not Application.Terminated do begin
     // B実行中
+
     // B 処理中 ...
 
     Sleep(1500);
 
     Sleep(1500);
 
     inc(Counter);
 
     inc(Counter);
     // Aを起こす
+
     // A 起動
 
     RtlEventSetEvent(Form1.ThreadA.WaitForB);
 
     RtlEventSetEvent(Form1.ThreadA.WaitForB);
 
   end;
 
   end;
 +
end;</syntaxhighlight>
 +
 +
{{Note|RtlEventWaitFor の前に RtlEventSetEvent を呼ぶことはできますが、その場合 RtlEventWaitFor は即座に制御を返します。終了フラグをクリアしたいのでしたら RTLeventResetEvent を使用してください。}}
 +
 +
== Fork ==
 +
マルチスレッドアプリケーションにおける fork に関して、次のことに留意してください。fork (あるいはfpFork)を呼び出す<B>前</B>に生成し動作させたあらゆるスレッドは、子プロセスで走っているのでは<B>ありません</B>。fork() の man ページで述べられているように、fork を呼び出す前に走っているスレッドの状態は未定義になります。
 +
 +
従って、fork を呼び出す前に初期化されたスレッドは、intialization 節を含め、動作しません。
 +
 +
==並列手続き/ループ==
 +
マルチスレッドの内の特殊なもので、単一の手続きを並列実行するものです。[[Parallel procedures/ja|並列手続き]]をご覧ください。
 +
 +
==分散コンピューティング==
 +
マルチスレッドを更に踏み込んだもので、複数のマシンでスレッドを稼動させます。
 +
* synapse、lnet、indy のような TCP スイートのいずれかを通信に使用します。これらは柔軟性に富み、クライアントとサーバのアプリケーションの接続の自由度上げたい場合よく用いられます。
 +
* [[MPICH]] のようなメッセージパッシングライブラリを使用します。MPICH はコンピュータクラスタ上で高性能計算をするのに使われています。
 +
 +
 +
 +
==外部スレッド==
 +
Free Pascal のスレッディングシステムを正しく動作させるには、新たに生成された FPC スレッドを初期化する必要があります(より正確には、スレッド毎に例外・I/O・スレッド変数 threadvar システムを初期化し、スレッド変数とヒープを動作可能にしなければなりません)。この過程は BeginThread を(あるいは間接的に TThread クラスを)用いると完全に自動的に行われます。しかし BeginThread を通さす生成されたスレッド(外部スレッド)を利用する場合、追加の作業が(いまのところ)必要となります。外部スレッドには外部の C ライブラリ (.DLL/.so) が生成したものも含みます。
 +
 +
外部スレッドを用いる際に考慮すべき点です(将来のコンパイラのヴァージョンでは不要になるかもしれません):
 +
 +
* 外部スレッドを一切使わない - FPC スレッドを使う。スレッドの生成を完全に制御できるなら、BeginThread を使って自分でスレッドを生成します。
 +
 +
* 呼出規約が一致しない場合(生成しようとするスレッド関数が cdecl 呼出規約を必要としているのに、BeginThread が Pascal 呼出規約を必要としているような場合)、レコード型を一つ作り、オリジナルのスレッド関数をその中に入れて、Pascal スレッド関数側から次のように呼びます:
 +
 +
<syntaxhighlight lang=pascal>type
 +
TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
 +
 +
PCdeclThreadFuncData = ^TCdeclThreadFuncData;
 +
TCdeclThreadFuncData = record
 +
  Func: TCdeclThreadFunc;  //cdecl 関数
 +
  Data: Pointer;          //ユーザデータ
 +
end;
 +
 +
// Pascal スレッドは cdecl 関数で呼ばれます
 +
function C2P_Translator(FuncData: pointer) : ptrint;
 +
var
 +
  ThreadData: TCdeclThreadFuncData;
 +
begin
 +
  ThreadData := PCdeclThreadFuncData(FuncData)^;
 +
  Result := ptrint(ThreadData.Func(ThreadData.Data));
 
end;
 
end;
</pre>
 
  
= Fork =
+
procedure CreatePascalThread;
 +
var
 +
  ThreadData: PCdeclThreadFuncData;
 +
begin
 +
  New(ThreadData);
 +
  // cdecl スレッド関数でなければなりません
 +
  ThreadData^.Func := func;
 +
  ThreadData^.Data := user_data;
 +
  // Pascal スレッドを作成
 +
  BeginThread(@C2P_Translator, ThreadData );
 +
end;</syntaxhighlight>
 +
 
  
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.
+
* ダミーのスレッドを一つ生成して、FPC のスレッドシステムを初期化します。アプリケーションの中で Pascal スレッドを一つでも生成しないと、スレッドシステムは初期化されず、スレッド変数もヒープも正しく動作しません。
  
So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.
+
<syntaxhighlight lang=pascal>type
 +
  tc = class(tthread)
 +
    procedure execute;override;
 +
  end;
  
= Parallel procedures/loops =
+
  procedure tc.execute;
 +
  begin
 +
  end;
  
A special case of multi threading is running a single procedure in parallel. See [[Parallel procedures]].
+
{ メインプログラム }
 +
begin
 +
  { スレッドの初期設定 }
 +
  with tc.create(false) do
 +
  begin
 +
    waitfor;
 +
    free;
 +
  end;
 +
  { ... コードが続く }
 +
end.</syntaxhighlight>
  
= Distributed computing =
+
(スレッドシステムの初期化が終わると、ランタイムライブラリはシステム変数 IsMultiThread を true にします。この変数はロックを行うのに FPC のルーチンのそこかしこで用いられます。この変数を手動で設定してはいけません。) 
  
The next higher steps after multi threading is running the threads on multiple machines.
 
* You can use one of the TCP suites like synapse, lnet or indy for communications. This gives you maximum flexibility and is mostly used for loosely connected Client / Server applications.
 
* You can use message passing libraries like [[MPICH]], which are used for HPC (High Performance Computing) on clusters.
 
  
----
+
* なんらかの理由でこれが上手く行かなかったら、次のコードを外部スレッド関数の中で使ってみてください:
[[Streaming components/ja]]
+
 
 +
<syntaxhighlight lang=pascal>function ExternalThread(param: Pointer): LongInt; stdcall;
 +
var
 +
  tm: TThreadManager;
 +
begin
 +
  GetThreadManager(tm);
 +
  tm.AllocateThreadVars;
 +
  InitThread(1000000); // ここでスタックサイズの初期値を調節します
 +
 
 +
  { スレッド化されたなにかをここで実行する ... }
 +
   
 +
  Result:=0;
 +
end;</syntaxhighlight>
 +
 
 +
 
 +
===外部スレッドの確認===
 +
時として、外部スレッドを取り扱わねばならないのか、はっきり分からないことがあります(例えば、C ライブラリがコールバックを使用しているなど)。これは、以下のようにして解析することができます。
 +
 
 +
1. アプリケーションの現在のスレッド ID を調べます。
 +
 
 +
<syntaxhighlight lang=pascal>Win32: GetCurrentThreadID();
 +
Darwin: GetThreadID(); 
 +
Linux: TThreadID(pthread_self);</syntaxhighlight>
 +
 
 +
2. スレッド関数内部で再度現在のスレッド ID を調べ、ステップ 1 の結果と比較します。
 +
 
 +
===タイムスライスの放棄===
 +
 
 +
ThreadSwitch 手続きを使ってください。
 +
 
 +
{{Note|Windows の裏技であるような ''Sleep(0)'' は使わないでください。これはすべてのプラットフォーム上で動作するものではないのです。}}
 +
 
 +
(訳注:タイムスライスは、1 回のスレッド処理に割り当てられる時間のことです。ThreadSwitch を実行すると残りの時間を放棄して、待機スレッドがあればそれらを行ってから、なければすぐに制御が戻ってきます。)
 +
 
 +
==関連項目==
 +
 
 +
* [[Streaming components/ja|コンポーネントをストリームする]]
 +
* [[Manager Worker Threads System]]
 +
* [[Example of multi-threaded application: array of threads/ja|マルチスレッドアプリケーションの例:スレッドの配列]]
 +
* [[Parallel procedures/ja|並列手続き]]
 +
* [[Main Loop Hooks/ja|メインループフック]]
 +
* [[Asynchronous Calls/ja|非同期呼び出し]]

Latest revision as of 00:31, 21 February 2020

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 を使ってマルチスレッドアプリケーションを記述したりデバッグしたりする方法について述べます。マルチスレッドアプリケーションは、同時作業させるために複数のスレッドを作成しているものを指します。もし、読者がマルチスレッドに初めて触れるのなら、是非マルチスレッドが本当に必要かどうかを「マルチスレッドが必要ですか?」節でお確かめください。そうすれば頭痛の種を激減することができるでしょう。

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

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

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

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

マルチスレッドが必要ですか?

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

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

マルチスレッドが必要なのは、次のような場合だけです。

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

マルチプロセッサによる同時処理で処理速度を上げるためにマルチスレッドを使いたいのであれば、プログラムが 1 つの CPU コアのリソースを 100% フルに使い切っているか確認してください(例えば、プログラムでファイルの書き込みなど入出力の処理が多く占めている場合、時間がかかる作業ではありますが CPU には負荷はかかっていません。このようなケースでマルチスレッドを使ってもプログラムは速くならないでしょう。)。また、最適化レベルが最大の 3 なっているかも確認してください。最適化レベルを 1 から 3 に切り替えたら、プログラムが約 5 倍速くなったことがありました。

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

Windows では動作させるのに必要なユニットは特にありません。Linux、Mac OS X、FreeBSD では cthreads ユニットが必要で、プロジェクトユニット(プログラムソース、通常は .lpr ファイル)の一番最初に必ず置かなくてなりません!

Lazarus アプリケーションコードでは以下のようになるでしょう。(訳注:実際に見れば分かりますが、cthreads は自動生成された時点で既に UseCThreads スイッチ付きで組み込まれています。)

program MyMultiThreadedProgram;
{$mode objfpc}{$H+}
uses
{$ifdef unix}
  cthreads,
  cmem, // the c memory manager is on some systems much faster for multi-threading
{$endif}
  Interfaces, // this includes the LCL widgetset
  Forms
  { you can add 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 節に置き、アプリケーションをコンパイルし直してください。
Light bulb  Note: "mcount" が見つからないというようなリンカエラーが発生することがあります。これが発生するのは、マルチスレッドコードを含むユニットを使っていて、cthreads ユニットの追加もしくはスマートリンクの使用が必要な場合です。
Light bulb  Note: 手続き SYSTEM_NOTHREADERROR 内でエラー "Project raised exception class 'RunError(232)'" が発生することがあります。これが発生するのは、コード内でスレッドが必要としており、cthreads ユニットの追加が必要な場合です。

TThread クラス

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

マルチスレッドアプリケーションは TThread クラスを使用すると簡単に作成できます。このクラスを用いると追加のスレッド(メインスレッドと並列に実行する)を作ることができます。そのためには通常、Create コンストラクタと、Execute メソッドの 2 つのメソッドをオーバーライドしなければなりません。

コンストラクタは、スレッドを準備する際に使います。必要となる変数ないしプロパティに初期値を設定します。基となる TThread のコンストラクタは Suspended という引数を必要とします。名前からもお分かりのように、Suspended が False の場合、スレッドは作成後直ちに実行されます。逆に Suspended が True の場合、スレッドは停止状態で作成されます。この場合、スレッドを実行するには Start メソッドを呼びます。

Light bulb  Note: メソッド Resume は FPC 2.4.4 以降非推奨になりました。Resume は Start に置き換えてください。

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

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

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

Terminated : boolean;

です。

Terminated のデフォルト値は False です。通常の場合、スレッドはループを持っていますが、Terminated が True になったら、そのループから脱出しなければなりません。したがってループのサイクル毎に、Terminated が True になっていないかチェックする必要があります。もし True になっていたなら、速やかに必要な終了処理を行い、Execute メソッドを終了させなければなりません。ですから、Terminate メソッドを呼び出してもデフォルト状態では何も起きないことを、しっかり覚えておいてください。Execute メソッド自体が自分自身を終了するように明示的に実装しなければなりません。

先に説明したように、スレッドはビジュアルコンポーネントを操作すべきではありません。なにかをユーザに表示するためにはメインスレッドで行います。

これを行うために TThread には Synchronize というメソッドがあります。このメソッドは引数として、引数を持たないメソッドを 1 つとります。例えば、MyMethod というメソッドをスレッドから実行するには、Synchronize(@MyMethod) として呼び出します。この時スレッドの実行は一時的に停止し、メインスレッドから当該メソッドが実行され、その後スレッド実行が再開されます。

Synchronize の正確な動作はプラットフォーム依存しますが、基本的には以下のようになります。

  • メッセージをメインメッセージキューに投げ、スレッドは休眠状態になります。
  • メインスレッドでメッセージが処理され、MyMethod が呼ばれます。MyMethod は(デバイスや描画用の)コンテクスト無しで実行されます。つまり、MouseDown イベントや Paint イベント実行中に割り込んで実行されるのではなく、これらのイベントは後で実行されます。
  • メインスレッドで MyMethod が実行された後、スレッドの休眠状態が解かれ、次のメッセージが処理されます。
  • スレッドの処理が再開されます。

TThread が持つもう一つの重要なプロパティに FreeOnTerminate があります。このプロパティを True にしておくと、スレッドの実行(Execute メソッド)が停止した後にスレッドオブジェクトは自動的に解放されます。これを行わない場合は、アプリケーション側で手動でオブジェクトを解放する必要があります。
(訳注:以下の例では単に FreeOnTerminate = True しているだけなので、スレッドが終了した場合 MyThread 変数が示すアドレスに不正にアクセスできてしまいます。スレッド終了時に必ず呼ばれる TThread.OnTerminate で MyThread := nil するなどして適切な処置を行ってください。OnTerminate イベントも Synchronize を通して呼ばれています。)

例:

  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;
  // このメソッドはメインスレッドで実行されるので、すべての GUI 要素にアクセス可能です。
  begin
    Form1.Caption := fStatusText;
  end;
 
  procedure TMyThread.Execute;
  var
    newStatus : string;
  begin
    fStatusText := 'TMyThread Starting...';
    Synchronize(@Showstatus);
    fStatusText := 'TMyThread Running...';
    while (not Terminated) and ([他のループ終了条件]) do
      begin
        ...
        [ここにスレッドループの本体コードを記述します]
        ...
        if NewStatus <> fStatusText then
          begin
            fStatusText := newStatus;
            Synchronize(@Showstatus);
          end;
      end;
  end;

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

  var
    MyThread : TMyThread;
  begin
    MyThread := TMyThread.Create(True); // このようにすると自動的に開始しません
    ...
    [ここにスレッドを実行する前に必要な初期化コードを記述します]
    ...
    MyThread.Start;
  end;

もっと柔軟な制御を望むなら、スレッドに対しイベントを作成します。こうすることで、イベントに対してイベントハンドラで設定できるようになり、Synchronize されたメソッドは特定のフォームやクラスに縛られなくなります。以下が例です。

  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;
  // このメソッドはメインスレッドで実行されるので、すべての GUI 要素にアクセス可能です。
  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 ([他のループ終了条件]) do
      begin
        ...
        [ここにスレッドループの本体コードを記述します]
        ...
        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;

    // FreeOnTerminate が true であれば、以下の記述は必要ありません。
    // MyThread.Free;
    inherited;
  end;

  procedure TForm1.Button1Click(Sender: TObject);
  begin
   MyThread.Start;
  end;

  procedure TForm1.ShowStatus(Status: string);
  begin
    Label1.Caption := Status;
  end;

特に注意すべきこと

Windows におけるスタックチェック

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

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

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

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

パッケージでのマルチスレッド

マルチスレッドを使用しているパッケージには、-dUseCThreads フラグをカスタム使用オプションに追加してください。パッケージエディタを開いて、オプション → 使用 → カスタム、そこへ -dUseCThreads を追加します。これによって、このパッケージを使用している IDE を含めたすべてのプロジェクトおよびパッケージでこのフラグが定義されます。IDE および IDE によって作成されたアプリケーションには、.lpr ファイル内に以下のコードが組み込まれています。(訳注:cmem は自動生成で組み込まれてはいません(Lazarus 1.0.12/1.2RC1 で確認)。)

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  cmem, // the c memory manager is on some systems much faster for multi-threading
  {$ENDIF}{$ENDIF}

Heaptrc

cmem ユニットを使った上で -gh スイッチを使うことはできません。-gh スイッチは、(メモリーリークを調べるために)ヒープマネージャーを拡張する heaptrc ユニットを使用するものです。ですから(リークチェックしたいのであれば)、cmem ユニットを使ってから その後に heaptrc ユニットを置くようにする必要があります。(訳注:heaptrc は cmem を考慮するように作られているけど、cmem はそうではないからでしょう。)

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  cmem, // the c memory manager is on some systems much faster for multi-threading
  {$ENDIF}{$ENDIF}
  heaptrc,

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

難しく考える必要はありません。このページで紹介されているマルチスレッドのやり方でアプリケーションを適切な動作をさせればいいのです。SMP は常に有効になっていますから。

Lazarus でのマルチスレッドアプリケーションのデバッグ

LazarusでのデバグにはGDBが必要です。Lazarusのデバグ環境はどんどんフル装備かつ安定になっています。しかしながら Linux の一部のディストリビューションには問題をかかえたものもあります。 (* 訳注: MacOSX 10.8以降の場合、Xcode から GDB が失われているため、ユーザが自力で GDB をインストールし、デバグのための特権を与える必要があります *)

デバッグ出力

単一スレッドアプリケーションでは、単純にコンソールでもターミナルでも好きなものに出力すれば、出力した順に行が並びます。マルチスレッドアプリケーションでは事態はもっと複雑です。スレッドAがスレッドBより先に一行出力したからといって、その順に書き込まれるとは限りません。あるスレッドが自分の出力を書いているときに、たまたま他のスレッドが行を書き込んでいるかも知れないのです。Linux では(多分)適切なDebugLn() 出力を得られるでしょう。Win32 ではDegubLn() をメインスレッドの外で使っていることによる例外が発生するかもしれません(おそらく DiskFull 例外)。頭痛を軽減するには、下記の DebugLnThreadLog() をお使いください。

The 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サーバの Desktop Manager がハングするかもしれません。例えば、デバグ中のアプリケーションがマウスやキーボードを捕獲し、そこで gdb がアプリケーションを一時停止させ、Xサーバがアプリケーションを待ってしまうとこうなります。こうなってしまったら、他のコンピュータからログインするか、CTRL+ALT+F3 でそのセッションから抜け出すかして、gdb を kill してください。あるいは、ウィンドウマネジャを再起動することもできます: 次のように入力してください sudo /etc/init.d/gdm restart。これで Desktop Manager が再起動し、デスクトップに戻れます。

この問題は、gdb がデバグ中のプログラムのどこで実行を止めるかに依存するので、ちょっとしたトリックが有効な場合があります: Ubuntu x64 では、プロジェクトオプション → コンパイラオプション → リンク → デバッグ:外部の gdb デバッグシンボルファイルを使用 (-Xg) をチェックします。

別のXデスクトップを開く手もあります。IDEとgdbを動かすデスクトップと、アプリケーションを動かすデスクトップを分けるのです。そうすれば固まるのはテスト用のデスクトップだけです。Xの別インスタンスを作るには:

 X :1 &

とします。元のデスクトップに戻るには CTRL+ALT+F7を、再び新しいデスクトップに行くには CTRL+ALT+F8 を使います (このキーコンビネーションがうまく行かなかったら、CTRL+ALT+F2 を試してください... Slackwareで有効でした)。

これで、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 で開き、アクセス制御がディセーブルされます。

Lazarus ウィジェットセットインターフェース

win32、gtk、carbon インターフェースはマルチスレッドをサポートしています。それは TThread、クリティカルセクション、同期動作のことを指しています。しかしながら、これらはスレッドセーフではないのです。つまり、ある瞬間に同時に LCL にアクセスできるスレッドは 1 つに限られているということです。また、メインスレッドが他のスレッドに対して待機するようなことは通常はなく、メインスレッドだけが LCL へのアクセスを許可されているのです。TControl、アプリケーション、LCL ウィジェットの各ハンドルを使って動作させられるのはメインスレッドだけなのです。その一方で、LCL にはスレッドセーフな関数もいくつかあります。例えば、FileUtil ユニット内のほとんどの関数はスレッドセーフです。

スレッド間のやり取りに SendMessage/PostMessage を使用する

1 つのアプリケーションにおいて LCL API を呼び出すのはただ 1 つであるべきで、通常はメインスレッドがそれを担っています。いくつかの間接的手段を通すことで、他のスレッドでも LCL の使用が可能になります。SendMessage や PostMessage の使用もそのような手段の一つといえるでしょう。LCLIntf.SendMessage と LCLIntf.PostMessage は、メッセージをアプリケーションのメッセージプールに送り、ウィンドウを直接制御するものです。

これらのルーチンのドキュメントもお読みください。

SendMessage と PostMessage の違いは、呼び出した側への制御の復帰方法です。SendMessage ではメッセージを送った先のウィンドウの処理が完了するまで制御が戻りませんが、PostMessage では即座に制御が戻ります。

以下は、別のスレッドよりメインスレッドへ LCL コントロールに表示させるテキストを送る方法の例です。

const
  WM_GOT_ERROR           = LM_USER + 2004;
  WM_VERBOSE             = LM_USER + 2005;

procedure VerboseLog(Msg: string);
var
  PError: PChar;
begin
  if MessageHandler = 0 then Exit;
  PError := StrAlloc(Length(Msg)+1);
  StrCopy(PError, PChar(Msg));
  PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
end;

こちらは、受け取ったメッセージの処理の例です。

const
  WM_GOT_ERROR           = LM_USER + 2004;
  WM_VERBOSE             = LM_USER + 2005;

type
  { TformConsole }

  TformConsole = class(TForm)
    DebugList: TListView;
    // ...
  private
    procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
  end;

var
  formConsole: TformConsole;

implementation

....

{ TformConsole }

procedure TformConsole.HandleDebug(var Msg: TLMessage);
var
  Item: TListItem;
  MsgStr: PChar;
  MsgPasStr: string;
begin
  MsgStr := PChar(Msg.wparam);
  MsgPasStr := StrPas(MsgStr);
  Item := DebugList.Items.Add;
  Item.Caption := TimeToStr(SysUtils.Now);
  Item.SubItems.Add(MsgPasStr);
  Item.MakeVisible(False);
  //f/TrayControl.SetError(MsgPasStr);
end;

end.

クリティカルセクション

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

以下はクリティカルセクションの一般的な使い方です。

セクションを宣言します(すべてのスレッドからアクセスできるようにグローバルにします)。

 MyCriticalSection: TRTLCriticalSection;

セクションを作成します。

 InitializeCriticalSection(MyCriticalSection);

スレッドを実行します。排他的な処理は以下のようにします。

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

すべてのスレッドが停止した後、セクションを解放します。

 DeleteCriticalSection(MyCriticalSection);

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

例:5 つのスレッドが 1 つのカウンターに対して増加処理を行います。
lazarus/examples/multithreading/criticalsectionexample1.lpi

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

変数の共用

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

例:5 つのスレッドが 1 つのカウンターに対して増加処理を行います。
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 が A を起動させるまで待機し続けます
    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;
Light bulb  Note: RtlEventWaitFor の前に RtlEventSetEvent を呼ぶことはできますが、その場合 RtlEventWaitFor は即座に制御を返します。終了フラグをクリアしたいのでしたら RTLeventResetEvent を使用してください。

Fork

マルチスレッドアプリケーションにおける fork に関して、次のことに留意してください。fork (あるいはfpFork)を呼び出すに生成し動作させたあらゆるスレッドは、子プロセスで走っているのではありません。fork() の man ページで述べられているように、fork を呼び出す前に走っているスレッドの状態は未定義になります。

従って、fork を呼び出す前に初期化されたスレッドは、intialization 節を含め、動作しません。

並列手続き/ループ

マルチスレッドの内の特殊なもので、単一の手続きを並列実行するものです。並列手続きをご覧ください。

分散コンピューティング

マルチスレッドを更に踏み込んだもので、複数のマシンでスレッドを稼動させます。

  • synapse、lnet、indy のような TCP スイートのいずれかを通信に使用します。これらは柔軟性に富み、クライアントとサーバのアプリケーションの接続の自由度上げたい場合よく用いられます。
  • MPICH のようなメッセージパッシングライブラリを使用します。MPICH はコンピュータクラスタ上で高性能計算をするのに使われています。


外部スレッド

Free Pascal のスレッディングシステムを正しく動作させるには、新たに生成された FPC スレッドを初期化する必要があります(より正確には、スレッド毎に例外・I/O・スレッド変数 threadvar システムを初期化し、スレッド変数とヒープを動作可能にしなければなりません)。この過程は BeginThread を(あるいは間接的に TThread クラスを)用いると完全に自動的に行われます。しかし BeginThread を通さす生成されたスレッド(外部スレッド)を利用する場合、追加の作業が(いまのところ)必要となります。外部スレッドには外部の C ライブラリ (.DLL/.so) が生成したものも含みます。

外部スレッドを用いる際に考慮すべき点です(将来のコンパイラのヴァージョンでは不要になるかもしれません):

  • 外部スレッドを一切使わない - FPC スレッドを使う。スレッドの生成を完全に制御できるなら、BeginThread を使って自分でスレッドを生成します。
  • 呼出規約が一致しない場合(生成しようとするスレッド関数が cdecl 呼出規約を必要としているのに、BeginThread が Pascal 呼出規約を必要としているような場合)、レコード型を一つ作り、オリジナルのスレッド関数をその中に入れて、Pascal スレッド関数側から次のように呼びます:
type
 TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;

 PCdeclThreadFuncData = ^TCdeclThreadFuncData;
 TCdeclThreadFuncData = record
   Func: TCdeclThreadFunc;  //cdecl 関数
   Data: Pointer;           //ユーザデータ
 end;

// Pascal スレッドは cdecl 関数で呼ばれます
function C2P_Translator(FuncData: pointer) : ptrint;
var
  ThreadData: TCdeclThreadFuncData;
begin
  ThreadData := PCdeclThreadFuncData(FuncData)^;
  Result := ptrint(ThreadData.Func(ThreadData.Data));
end;

procedure CreatePascalThread;
var
  ThreadData: PCdeclThreadFuncData;
begin
  New(ThreadData);
  // cdecl スレッド関数でなければなりません
  ThreadData^.Func := func;
  ThreadData^.Data := user_data;
  // Pascal スレッドを作成
  BeginThread(@C2P_Translator, ThreadData );
end;


  • ダミーのスレッドを一つ生成して、FPC のスレッドシステムを初期化します。アプリケーションの中で Pascal スレッドを一つでも生成しないと、スレッドシステムは初期化されず、スレッド変数もヒープも正しく動作しません。
type
   tc = class(tthread)
     procedure execute;override;
   end;

   procedure tc.execute;
   begin
   end;

{ メインプログラム } 
begin
  { スレッドの初期設定 }
   with tc.create(false) do
   begin
     waitfor;
     free;
   end;
   { ... コードが続く } 
end.

(スレッドシステムの初期化が終わると、ランタイムライブラリはシステム変数 IsMultiThread を true にします。この変数はロックを行うのに FPC のルーチンのそこかしこで用いられます。この変数を手動で設定してはいけません。)


  • なんらかの理由でこれが上手く行かなかったら、次のコードを外部スレッド関数の中で使ってみてください:
function ExternalThread(param: Pointer): LongInt; stdcall;
var
  tm: TThreadManager;
begin
  GetThreadManager(tm);
  tm.AllocateThreadVars;
  InitThread(1000000); // ここでスタックサイズの初期値を調節します
  
  { スレッド化されたなにかをここで実行する ... }
    
  Result:=0;
end;


外部スレッドの確認

時として、外部スレッドを取り扱わねばならないのか、はっきり分からないことがあります(例えば、C ライブラリがコールバックを使用しているなど)。これは、以下のようにして解析することができます。

1. アプリケーションの現在のスレッド ID を調べます。

Win32: GetCurrentThreadID();
Darwin: GetThreadID();  
Linux: TThreadID(pthread_self);

2. スレッド関数内部で再度現在のスレッド ID を調べ、ステップ 1 の結果と比較します。

タイムスライスの放棄

ThreadSwitch 手続きを使ってください。

Light bulb  Note: Windows の裏技であるような Sleep(0) は使わないでください。これはすべてのプラットフォーム上で動作するものではないのです。

(訳注:タイムスライスは、1 回のスレッド処理に割り当てられる時間のことです。ThreadSwitch を実行すると残りの時間を放棄して、待機スレッドがあればそれらを行ってから、なければすぐに制御が戻ってきます。)

関連項目