GUI とマルチスレッド、GUI ツールキットの振る舞い
はじめに
プラットフォームによっては、GUI のコントロールはコントロールが所属しているスレッド以外から操作することは安全ではないことが多い*1。私が Windows でこのことを知ったが、どうやら大抵のプラットフォームで同じことが言える。これはおそらく、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は以下のような手順を踏んでいる。
- スレッドがメインスレッド ( GUI スレッド ) と同じスレッド ID かどうかをチェックする
- 同じならそのまま実行する
- SynchronizeCritSect クリティカルセクションに入る
- SynchronizeMethod に引数で指定されたメソッドポインタをコピーする
- DoSynchronizeMethod フラグをたてる
- SynchronizeTimeoutEvent をシグナルする
- WakeMainThread 関数にメソッドポインタを渡す
- ExecuteEvent がシグナルされるまで無限に待つ
- LocalSyncException に SynchronizeException をコピーする
- SynchronizeCritSect クリティカルセクションを終える
- 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 する
で、CheckSynchronize が呼び出されると、ようやく TThread.Synchronize で指定したメソッドがメインスレッドで実行される。CheckSynchronize の内部はこうなっている。
- スレッドIDがメインスレッドのものと同じかをチェックする
- 違ったら例外をはく
- SynchronizeTimeoutEvent を非シグナル状態にする
- DoSynchronizeMethod フラグが立っていれば、以下の処理を行う
- DoSynchronizeMethod フラグを false にする
- try catch で囲んで SynchronizeMethod する
- 例外を受け取ったらそれを SynchronizeException に保存しておく
- 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 スレッドを止めてから他のスレッドでコントロールする方式のようだ。これでうまく動くのだろうか・・・?内部では以下のように処理が進む。
- wxMutexGuiEnter
- wxWakeUpMainThread
- PostThreadMessage(WM_NULL)
- メインループがWM_NULLを受け取る
- theWxApp->ProcessPendingEvents
- ここまでくると処理が実行される
- メインループがwxMutexGuiLeaveOrEnterを実行する
- wxWakeUpMainThread
- wxMutexGuiLeave
この二つの関数に関しては、以下で少し触れられていて、これをやるよりも wxPostEvent を使った方がいいらしい。納得。
SWT ( Java )
Eclipse の登場で脚光を浴びた感のある SWT。以下のようなメソッドが提供されている。
- Display.syncExec
- Display.asyncExec
以下が参考になる。