GUI とマルチスレッド、GUI ツールキットの振る舞い

はじめに

プラットフォームによっては、GUI のコントロールはコントロールが所属しているスレッド以外から操作することは安全ではないことが多い*1。私が Windows でこのことを知ったが、どうやら大抵のプラットフォームで同じことが言える。これはおそらく、GUI を使ったシステムはそのためのメインループを何らかの形で持っており、そこに割り込むことが危険であるためであろう。今回は、それに関して有名な GUI ツールキットがどのように対処しているかを見ていく。

主な対処方法

  1. GUI スレッドで実行する
    • 呼び出し元でスレッドのコンテキストを切り替え、GUI スレッドで GUI コントロールを操作し、終わったら呼び出し元スレッドの実行を再開する
    • 一番メジャーで実装しやすい
  2. 別スレッドで実行する
    • GUI スレッドを何らかの形で停止し、そのときに別スレッドで GUI コントロールを操作し、終わったら GUI スレッドの実行を再開する

次に、GUI ツールキットごとの対処方法を見てみよう。

VCL ( Delphi )

Delphi は、Professional 以上のエディションを購入すると VCLソースコードがついてくるという特典があった*2。手元に Delphi の環境がないのでソースコードを見ることができないが、現在は Free Delphi とでもいえる Lazarus というものがある。これに VCL に非常によく似たソースコードが入っているので、これについて見てみた。


VCL でスレッドを扱うときは、TThread というクラスを使う。これには Synchronize というメソッドがあり、これに関数を指定してやると、それが GUI スレッド上で実行される、という仕組みだ。しかし、このメソッドにはどの GUI スレッドで実行するかという指定がない。つまり、VCL のシステムに暗黙の何かがあるということだ。これはどうなっているのだろうか?


Lazarus をインストールすると、Delphi で利用可能な言語である Object Pascalコンパイラ、Free Pascal もインストールされ、そこにソースがある。

  • fpc/source/rtl/objpas/classes/classes.inc


TThread.Synchronizeは以下のような手順を踏んでいる。

  1. スレッドがメインスレッド ( GUI スレッド ) と同じスレッド ID かどうかをチェックする
    1. 同じならそのまま実行する
  2. SynchronizeCritSect クリティカルセクションに入る
    1. SynchronizeMethod に引数で指定されたメソッドポインタをコピーする
    2. DoSynchronizeMethod フラグをたてる
    3. SynchronizeTimeoutEvent をシグナルする
    4. WakeMainThread 関数にメソッドポインタを渡す
    5. ExecuteEvent がシグナルされるまで無限に待つ
    6. LocalSyncException に SynchronizeException をコピーする
  3. SynchronizeCritSect クリティカルセクションを終える
  4. LocalSyncException が何かあれば、例外を送出する


WakeMainThread というのは、以下にある。これはプラットフォーム毎に実装が異なり、Lazarus 側にソースがある。以下で、Windows の場合を取り上げる。

  • lazarus/lcl/interfaces/win32/win32object.inc
    • WM_NULL を AppHandle に向かって PostMessage する


で、以下に処理がうつる。

  • lazarus/lcl/interfaces/win32/win32callback.inc
    • CheckSynchronize する
      • これは中身がfpc/source/rtl/objpas/classes/classes.incにある
      • CheckPipeEventsする


で、CheckSynchronize が呼び出されると、ようやく TThread.Synchronize で指定したメソッドがメインスレッドで実行される。CheckSynchronize の内部はこうなっている。

  1. スレッドIDがメインスレッドのものと同じかをチェックする
    1. 違ったら例外をはく
  2. SynchronizeTimeoutEvent を非シグナル状態にする
  3. DoSynchronizeMethod フラグが立っていれば、以下の処理を行う
    1. DoSynchronizeMethod フラグを false にする
    2. try catch で囲んで SynchronizeMethod する
    3. 例外を受け取ったらそれを SynchronizeException に保存しておく
    4. ExecuteEvent をシグナルして、TThread 側に完了を通知する


複雑だが、見れば見るほどよく出来ている。しかし、いい意味でも悪い意味でも VCL システムにべったりと依存しているので、この仕組だけを取り出すということは難しそうである。

.NET

Control.Invoke メソッドを使うと、スレッドのコンテキストスイッチを切り替えて GUI スレッド上でデリゲートを同期的に実行できる。また、Control.BeginInvoke メソッドを使うと同じようなことが非同期的に実行できる。内部の仕組みは Mono のソースコードなどを見れば分かるだろうが、見ていない。

Qt

Qt は最も普及しているクロスプラットフォームに対応した GUI ツールキットだと思う。Qt の場合、QThread::postEvent メソッドというものが用意されている。これはスレッドセーフであるらしい。


このメソッドは、対象となるウィジェットとイベントを指定するという、VCL の TThread.Synchronize に比べると随分素直な作りになっている。関数やらメソッドやらを指定して実行するタイプではない。このイベントというのは、QEvent というクラスで、Windows メッセージを抽象化したようなクラスである。イベントタイプにより何であるかを識別し、それ毎に任意の情報を与えることができるようになっている。


作りとしては安全だが、TThread.Synchronize、Control.Inboke に比べると利便性は落ちるような気がする。他にも何か方法が提供されているのだろうか?

wxWidgets

クロスプラットフォームに対応したフリーな GUI ツールキットのひとつ。wxWidgets では、wxwxMutexGuiEnter/wxMutexGuiLeaveを使い、これらで挟まれたスコープ内に処理を書く、という少し変わった実装になっている。


何か変だなと違和感を感じたので、Windows での実装のソースコードを追ってみたところ、GUI スレッドでコントロールを操作するのではなく、GUI スレッドを止めてから他のスレッドでコントロールする方式のようだ。これでうまく動くのだろうか・・・?内部では以下のように処理が進む。

  1. wxMutexGuiEnter
    1. wxWakeUpMainThread
      1. PostThreadMessage(WM_NULL)
      2. メインループがWM_NULLを受け取る
      3. theWxApp->ProcessPendingEvents
        1. ここまでくると処理が実行される
      4. メインループがwxMutexGuiLeaveOrEnterを実行する
  2. wxMutexGuiLeave


この二つの関数に関しては、以下で少し触れられていて、これをやるよりも wxPostEvent を使った方がいいらしい。納得。

SWT ( Java )

Eclipse の登場で脚光を浴びた感のある SWT。以下のようなメソッドが提供されている。

  1. Display.syncExec
  2. Display.asyncExec
    • .NET の Control.Invoke/Control.BeginInvoke と同じような仕組み
    • JNI でネイティブな GUI を使うものなのでこうなっていると思われる

以下が参考になる。

Swing ( Java )

Java はあまり使っていないが、SwintUtilities.invokeLater というのがあるらしい。以下を見る限り、Control.Invoke と同じような仕組みだと思われる。

*1:操作によってはデッドロックしたりするはず

*2:私は Delphi 6 までしか知らないので現在の事情は分からない