Multithreaded Application Tutorial/ja

From Free Pascal wiki
Revision as of 16:16, 17 March 2014 by TheCreativeCAT (talk | contribs) (→‎外部スレッド: Translation Added)
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 を使ってマルチスレッドアプリケーションを記述したりデバッグしたりする方法について述べます。マルチスレッドアプリケーションは、同時作業させるために複数のスレッドを作成しているものを指します。もし、読者がマルチスレッドに初めて触れるのなら、是非マルチスレッドが本当に必要かどうかを「マルチスレッドが必要ですか?」節でお確かめください。そうすれば頭痛の種を激減することができるでしょう。

スレッドのうち 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 を実行すると残りの時間を放棄して、待機スレッドがあればそれらを行ってから、なければすぐに制御が戻ってきます。)

関連項目