マルチスレッド

Tclは、同時に複数のインタプリタを生成して、持たせることができますが、
通常は、各インタプリタを同じスレッドに結び付けます。
各インタプリタを別のスレッドに結びつけるには、Thread拡張を使います。
Thread拡張を使うと、異なるスレッド中のインタプリタ間で通信することができ、
別スレッドの結果を待ち合わせしたり、非同期で処理したりできます。

実験的な試み

実は、Tclのマルチスレッド機能の実験的な試みは、Tcl8.1のころからあり、
Tclソースコード中にtestthreadという隠しコマンドで実装されています。
しかし、マルチスレッド版Tclの実行形式がリリースされたことはありません。
Tcl8.3.1からtestthreadコマンドは、Thread拡張パッケージに変えられました。
いずれにしても、ソースコードからビルドする以外には使えないのです。

ソースからビルド

最近は、Windows上でもフリーなCコンパイラ(MinGWなど)を使って、
Tclをソースコードからビルドしたり、Tcl拡張をビルドしたりできるので、
わりとお気軽にTclのマルチスレッド機能を試すことができるようになりました。

Tcl8.3.4のソースコードで、マルチスレッド版のtclshとwishをビルドするには、
まず、Tcl coreを--enable-threadsオプションでconfigureしてmakeした後に、
tcltestとtktestをmakeします。
tcltest.exeとtktest.exeは、tclsh.exeとwish.exeのマルチスレッド版になります。

$ cd tcl8.3.4/win
$ ./configure --enable-gcc --enable-threads
$ make
$ make tcltest.exe

$ cd tk8.3.4/win
$ ./configure --enable-gcc --enable-threads
$ make
$ make tktest.exe

一方、Thread拡張をビルドするのも、configureをしてmakeするだけですが、
configureで生成されるMakefileが不完全のため、若干手直しが必要です。
手っ取り早く使いたい方は、TclアーカイブからThread2.2のバイナリが取れます。

仕様

Tcl coreのtestthreadコマンドより、Thread拡張の方が新しい仕様で、
コマンド数も多いので、Thread拡張の方をおすすめします。
Thread拡張のコマンド仕様はThread拡張のマニュアルを参照してください。
この中のthread::jointhread::transferは、Tcl8.4との組み合わせでないと使えません。
その他にもマニュアルに載っていない隠しコマンドがあります。
隠しコマンドは、予告なく仕様変更される可能性があるので、ご注意ください。

使用例

簡単なマルチスレッドのサンプルを示します。
スレッドを生成するには、createコマンドを使います。
引数のスクリプトを省略すると、イベント待ち状態のスレッドが生成されます。

package require Thread

::thread::create
=> 3308

スレッドが生成されるとスレッドIDが返却されます。

引数にスクリプトを指定すると、生成したスレッドでスクリプトを実行します。

package require Thread

::thread::create {set a 5}
=> 3309

スクリプトの実行が終わると、スレッドは破棄されます。

スクリプトの実行が終っても、スレッドを破棄したくなければ、
スレッドをイベント待ち状態にしておきます。
この時にvwaitを使ってもよいですが、waitコマンドが使えます。

package require Thread

::thread::create {set a 5; ::thread::wait}
=> 3310

別スレッドから待ち状態のスレッドにsendコマンドでスクリプトを送信できます。
sendコマンドの引数には、送信先のスレッドのスレッドIDとスクリプトを指定します。
通常、sendコマンドで送信したスクリプトの結果は同期して返却されます。
sendに-asyncオプションを付けると処理は非同期になり、スクリプトの結果は返却されません。

package require Thread

::thread::create {set a 5; ::thread::wait}
=> 3311
::thread::send 3311 {set a}
=> 5
::thread::send -async 3311 {set a}

この例は、別スレッドから別スレッドの変数の値を参照しています。

全スレッドのスレッドIDを一覧するには、namesコマンドを使います。

package require Thread

::thread::names
=> 3308 3310 3311

スレッドが存在するかのチェックはexistsコマンドを使います。
スレッドIDで指定したスレッドが存在する場合は1を、それ以外は0が返却されます。

package require Thread

::thread::exists 3308
=> 1

カレントスレッドのスレッドIDを取るには、idコマンドを使います。

package require Thread

::thread::id
=> 3307

スレッドを破棄するには、exitまたはunwindコマンドを使います。
(Thread 2.2から::thread::exitは::thread::unwindに変更されています)

package require Thread

::thread::create
=> 3312
::thread::send 3312 {::thread::unwind}
=> target thread died

createコマンドで生成したスレッドでスクリプトを実行中にエラーが発生し、
かつ、そのエラーがcatchで捕獲されていない場合、アプリが異常終了します。
スレッドで発生したエラーは、errorprocで指定したプロシジャで捕獲できます。
エラープロシジャを定義しておけば、アプリが異常終了するのを回避できます。

package require Thread

proc myerrproc {id info} {
    puts stderr [list $id $info]
}

::thread::errorproc myerrproc

::thread::create {hoge}
=> 3313
=> 3313 {invalid command name "hoge" while executing "hoge"}

エラープロシジャの引数は、エラーの発生したスレッドのスレッドIDとエラー情報になります。
この情報からスクリプトのバグを見つけ出すこともできるでしょう。

thread::jointhread::transferは、Tcl8.4との組み合わせでないと使えません。
joinは、指定したスレッドが終了するまで呼び出しスレッドの実行を一時停止するコマンドです。
transferは、オープンしたファイルハンドルを別スレッドに転送するコマンドです。

その他、マニュアルに載っていないのですが、以下の様な
各スレッドで変数を共有して扱うための隠しコマンドがあります。
sv_condとsv_mutexは変数アクセスの排他制御をするためのコマンドです。
詳細は下記の参考文献を参照してください。説明は省略します。(^^;)
それ以外のsv_コマンドは、コマンド名から機能が創造できると思います。

sv_get, sv_lappend, sv_incr, sv_append, sv_set, sv_unset, sv_array, sv_cond, sv_mutex

以下の例は各スレッドで変数を共有する例です。
共有する変数はすべて配列変数として扱い変数名と添え字名で指定します。

package require Thread

::thread::sv_set a 1 5
=> 5
::thread::create
=> 3314
::thread::send 3314 {::thread::sv_set a 1}
=> 5
::thread::send 3314 {::thread::sv_incr a 1}
=> 6

今後の課題

まだ、Tclのマルチスレッド機能は、POSIXスレッド(Pスレッド)のサブセットのようで、スレッド毎にプライオリティを設定することができません。
また、別スレッドにTkのWidgetパス名を転送することができないので、マルチスレッドでWidgetを共有することができません。
まだ、実験的な試みなのですが、Tcl8.4でこれらの改善を期待したいところです。

参考文献