Home > メモ(Perl関連) > ithread

Perlのithreadsについて

 ithreadsに関する日本語のサイトがあまりないので、何か適当にメモっておきます。

基本

上記の和訳についておかしな点がありましたら当サイトの管理者までご一報ください。でも直接perldocjpにcommitしていただければなお嬉しい。

サンプル

留意点(重要な基本事項)

できるだけ最新のPerlで使おう

ithreadを使うときは、できるだけ最新のPerlを使うべき。特に5.8.0はバグが多いので使わないほうがよい(forksプラグマの項も参考)。5.8.5より前のバージョンではスレッド生成に時間がかかる場合があるので、できれば5.8.5以降を使うのがよい。

また、最近のバージョン(5.8.7?)では、クロージャとiスレッドを使ってもPerlがクラッシュしなくなったとか。以下の例であってるのかどうかちょっと自信ありません。

 {
   my $thr = threads->new(sub{  });
   sub test {
     return $thr;
   }
 }

 my $thr1 = test();
 my $thr2 = test();
 $thr1->join();
 $thr2->join(); # 古いバージョンだとPerlがクラッシュ
                # 現在では致命的エラー"Thread already joined"で終了

5.8.8ではjoinした後にスレッドインタプリタが解放されるようになり、いくつかのメモリリークの問題が解決されたとあります。

スレッドのコンテキスト

new (create) した時のコンテキストがjoin時のコンテキストになる。 ……ので、joinの戻り値にリストを得ようと思ったらnewを呼び出すところでリストコンテキストにしなければなりません。

use threads; my $th1 = threads->new( sub{return 1,3,5;} ); print $th1->join; # 5 を表示(スカラーコンテキストだから) my ($th2) = threads->new( sub{return 1,3,5;} ); print $th2->join; # 135 を表示(リストコンテキストだから)

スレッドと値

変数はコピーされる

 変数はスレッド開始時にそのスレッド内にコピーされます。例え変数を使用しなくても内部的にはコピーされます。

 my $hoge;
 
 threads->new(sub{  })->join;
 # ↑スレッド内に$hogeはコピーされている!

スレッドアンセーフなモジュールを利用していると、これが原因でPerlがクラッシュすることも多々あります。

 さらに関数もコピーされます。詳細は関連情報の『Perlのithreadsでプログラミングをする前に知っておくべきこと』参照先を。

 sub test {  }
 
 print \&test;
 
 threads->new(sub{ print \&test; })->join;
 # ↑&testのアドレスが違う!

スカラー値以外の共有化

 ハッシュや配列、それらのリファレンスなどをshareで共有化すると、内部の値はクリアされます。よって、まず共有化したのち、保持するデータをいれていかなければなりません。

my $test = {foo => bar};
share($test);
print $test->{foo}; # 入っていない!

オブジェクトの共有

 Perl5.8.8の段階でthreads::sharedのドキュメントを見ると、blessは共有されたリファレンスをサポートしておらず、スレッドローカルなリファレンスをblessするだけでスレッド間に伝搬しないとあります(共有リファレンスをblessできないわけではありません。例えばThread::Semaphorは共有リファレンスをblessしてつくられています。単にある共有リファレンスをblessしても、他のスレッドで共有されているリファレンスまでblessされないということです)。

 my $obj : shared;
 
 $obj = &share({});
 
 # 子スレッドでblessすると……
 threads->new(sub {
     my $obj_in_thr : shared = shift;
     bless $obj_in_thr, 'Hoge';
 }, $obj)->join;
 
 # blessは反映されている?
 print ref($obj) , "\n";

 が、実際にはPerl5.8.1以降、共有されたハッシュリファレンスと配列リファレンスはblessが伝搬します。また、threads::shared 0.95でスカラーリファレンスについても作用するようになりました。

 ところで、既存のOOなCPANモジュールなどでblessされたオブジェクトをshare()で共有化してスレッド間に渡すとどうなるかというと、たいていの場合まともに動作しません(共有化された変数に入れられるデータタイプに制約があるから)。複雑なオブジェクトはスレッド間での共有が困難です。そこで(無茶な)解決方法ですが、管理人が書いたObject::SharableをPerl5.8.1以降で使うと、オブジェクトを共有化できます。このモジュールは、受け取ったオブジェクトをシリアライズしてクラス変数のハッシュに格納し、代わりにObject::Sharableオブジェクトを返します。メソッドが呼び出されるとAUTOLOADでオリジナルのメソッドを呼び出すので透過的に使えます。メソッド呼び出しの際に元のオブジェクトの状態を再度シリアライズします。難点は、メソッドが呼び出されるたびにデシリアライズとシリアライズが発生してしまうこと等々……実験的かつ冗談的モジュールです。また、GLOB型のオブジェクトには対応していません。

オブジェクトは二度(以上)死ぬ

 blessされたリファレンス、すなわちオブジェクトをスレッドに渡すと、そのスレッド内でオブジェクトが破棄される毎にDESTROYメソッドが呼び出されます。つまりPerlのマルチスレッドでは気を付けないと、オブジェクトのDESTROYメソッドが複数回呼び出される可能性があるわけです。回避方法の一つとしてはThread::Blessがあります。また、先に書いたObject::Sharableでは、オブジェクトが生成されたスレッドidとDESTROYが呼び出されるスレッドidを比較することで、そのオブジェクトを生成したスレッドでのみDESTROYを呼び出すようにしています。この辺りの考え方はThread::Blessを参考にしています。
 ところで、DESTROYメソッド内でthreads->tidすると、Perl5.8.0では正しくスレッドidを返しません。ですので、上記の方法はPerl5.8.0では使えません。5.8.1以降でお使いください(この辺はCLONEサブルーチンの場合と同じですね)。

Windows上でのdetach

Activeperlの5.8.3ではdetachの問題が修正されてます

Perl 5.8.4 以前のWindows上では、threads::detachが機能しません。そこでThread::Detach和訳)を使って暫定的な修正を行ないます。(Windows2000とActiveperl 5.8.0、5.8.1で試してみましたが、いまいちよくわかりません→どうもソースコードみると、期待通りに動かないように見えます。とりあえずバグレポート出してみました……)

特別なサブルーチンCLONEについて

スレッドセーフなモジュールにするために。非Perlデータ(例えばXSモジュールなどで)は、自動ではスレッド間でコピーされないのでCLONEを使う。

長くなったので後述

マルチスレッドにおけるrand()の使用

5.8.0でのバグ、および異なるスレッド間を通じて再現性のある数列を生成するモジュールについて

スレッドにおけるシグナル処理

 perlthrtutによれば、「シグナルとスレッドを混ぜるべきではない」ということ。実装がOS依存のためどうなるかわからない。これはPOSIXモジュールを使ってもまた然り。プロセス内であるスレッドから他のスレッドにシグナルを送るだけなら素直にcond_waitとcond_signalを使えば良いでしょう。一応Perlには標準モジュールでThread::Signalというのがありますが、これは古い5005thread用です。CPANにもithreads用のがありますが、あまり使えなさそうです。

 標準モジュールのThread::Signal

新しいコードでThread::Signalモジュールを使うのはお勧めできません。代わりに、threadsとその関連モジュールを直接利用することを勧めます。

しかし、threadsの新しい実装ではThread::Signalモジュールと等価なものがありません。明快な方法:シグナルは今や信頼できるかたちでthreadsを使わないPerlプログラミングに配信されます。新しいスレッド機能を使ったシグナル処理は、下支えしているスレッドの実装に追いついています。このスレッド実装はいままさに使われてるところなので、より信頼できないかもしれません。

スレッドに特化したシグナルを設定したいなら、他スレッドからのシグナルを処理したいスレッド内で、%SIGハッシュを変更します。これは少なくともLinuxでは動作しているようです。しかしながら保証はありませんし、これを使った場合の有益性もまちまちです。(5.8.4付属のThread::SignalのCAVEATより)

 Elizabeth Mattijsen氏のThread::Signal

このモジュールは、それぞれのスレッド毎に(擬似)プロセスが使われているシステムでしか動きません。私の知るところでは、これが起きるのは(古い)Linuxシステムだけです。この実装が動作する他のOSについても知りたいので、ドキュメントにこれを追加します。

Perl5.8.0ではシグナル処理に関するバグのため、%SIGハッシュのエントリは、そのスレッドから開始されるスレッド内で利用される前に代入されなければなりません。

シグナルがどんな場合にも実行されるかどうかはまだ明らかではありません。 とりわけ、%SIGをセットするまさにそのことが、他のタイムアウト(gethostbyaddr()を使った場合のような)や、他のシグナル(threads::shared::cond_wait()など)が実行されるまでシグナルが受け入れられない原因になります。 よって、このモジュールは(%SIGをセットする代わりに)POSIX::sigaction()を利用するように切り替えています。これは少なくとも、より信頼できるようにみえますし、%SIGをセットすることでそのシグナルが活性化しても普通にブロックされるシグナルを送るようにみえます。将来のPerlのバージョンでは%SIGを使ったシグナルの設定が修正されるでしょう。(CAVEATSより)

perl -V:cppflagsとやって-DTHREADS_HAVE_PIDSが出なければ動かないとのこと。仮に出ても動作は保証しないと。

関連情報

Where Wizards Fear To Tread by Artur Bergman

魔術師も踏むを恐れるところ
[適当な要約]

スレッドとモジュールの関係は?
 Perlのスレッドシステムは「明示的な共有データ」という考えに基づく。→デフォルトでスレッド間でデータを非共有。  これによりスレッドセーフなモジュールの開発を簡単にしてくれる。Pure-Perl(Perlだけで書かれている)なモジュールは本質的にスレッドセーフ。

スレッドセーフの3つのレベル

 ほとんどのPure-Perlはスレッドセーフになるだろうが、それでもソースをレビューするか、作者がスレッドセーフであると明言しない限りは信用するべきではない。また、全てのXSモジュールがスレッドアンセーフであるというわけでもない。

 Test::Simpleを例にしたスレッドセーフなモジュールの作りかた。

Things you need to know before programming Perl ithreads by liz

Perlのithreadsでプログラミングをする前に知っておくべきこと
[適当な要約]

5.8.0には共有配列に対してshift()を使ったさいに致命的なまでにメモリを食うバグが存在している。これは5.8.1でfixされている。

Perlのithreadsは軽くない。全てのデータ構造はコピーされる。このコピーは、スレッド内で局所化された変数の値が変化するとき(Copy On Write (COW))に生じるのではなく、スレッドの開始時に発生する。

共有変数は?
 →実際には共有していない。実は普通のtieされた変数。
  →余分にメモリを使う

モジュールを使うと全てのスレッドにモジュールがコピーされてしまう。そこでメモリの節約のためにモジュールを必要なスレッドの中だけで使う

use threads ();
threads->new( sub {
    require Benchmark; # use の代わりにrequireしてimport
    Benchmark->import;
    # 何か色々やる
} )->join;

作者は上記のやり方が好きではないので、Thread::Use日本語訳)モジュールを作成した。

モジュール

CPANには他にも色々なThread系モジュールがあります。またwin32では、Thread::Tieがうまく動作しないようです(後述)。そのため、それを利用するモジュールも正常に動かないっぽいのですが。

activeperlでtestを試したWindows上で使えるモジュール NAME ver 5.8.0 5.8.1 5.8.2 Thread::Running 1.05 x x o Thread::Bless 0.06 x o Thread::Exit 0.09 o* o * warn: scalars leaked Thread::Use 0.05 o o T::Queue::Any 0.07 o o T::Serialize 0.07 o o

 Thread::TieをWindowsのPerl5.8.1以降でどうにか動かす方法を見つけました。気が向いたら書きます。

その他

forkによるthreadsのエミュレート

 ithreadが効かないPerlをつかってたり、5.8.0のために致命的なメモリ消費をしてしまう場合、forksプラグマを試してみるのはどうでしょう? forkを使ってthreadsプラグマをエミュレートするので5.8以前のPerlでさえも(その場合Filterが必要ですが)threadsのAPIが利用できます。スレッド間の会話はSocket(TCP/IP)経由ですのでスピードは遅いですが、mod_perl/Apacheとも相性がよいらしいです。

cond_timedwait

 threads::sharedにcond_timedwait()が追加されました。タイムアウトの時間はエポック秒で絶対指定。何秒後とかの相対指定はできません。cond_waitに追加の引数をとる形式が追加されました(cond_timedwaitにも同様のものあり)。

なぜかスレッドの生成に時間がかかる時があるのですが…

 Perl5.8.4まで、スレッドの生成にかかる時間に大きな幅がありました。例えば1秒間に10個以上生成できることもあれば、1個しか生成できないときもあったりとか。これはスレッド複製ルーチンのハッシュアルゴリズムに問題があったためで、5.8.5で修正されました。
 その他にも5.8.5では、threads->create()に失敗したときにいきなりPerlが終了するのではなく、undefを返すように動作が変更になりました。(以上perl585deltaより)

Thread::Queueについて

 標準モジュールThread::Queueはスカラーしか扱えない。Thread::Queue::Anyは任意のデータ構造(のリファレンス)も扱うことができる。

 Thread::Queue::AnyはThread::Conveyorと名前が代わり、インターフェースも全く異なるものになりました。

CLONEサブルーチン

iスレッドはデータツリーをクローンすることで動作するので、異なるスレッド間でデータは共有されない。……あるスレッドがクローンされるときに、全Perlデータがクローンされる。しかしながら、非Perlデータは自動的にはクローンされない。5.7.2以降のPerlは、CLONEという特別なサブルーチンをサポートしている。CLONE内で、必要なあらゆること、例えば非Perlデータのクローン処理のようなことができる。CLONEは、それが定義されている(あるいはCLONEを継承している)パッケージ毎に一度だけ実行される。これは新しいスレッドのコンテキストで呼び出される。よって、あらゆる変更は新しい領域においてなされる。(perlmod 5.8.0)

以下のサンプルで試してみます。注意:以下はPerl5.8.0の場合です! Perl5.8.1以降は正しくカレントスレッド内でCLONEが実行されます。

#---- Mpdule 
package CloneTest;
use strict;
use warnings;
our $CLONE = 0;
sub CLONE{
    $CLONE++;
    print '$CLONE is ', $CLONE,
            " in Module.\t(tid is ", threads->tid, ")\n";
}
1;
#---- main.pl
$| = 1;
use warnings;
use strict;
use threads;
use CloneTest;

print '$CLONE is ', $CloneTest::CLONE,
        " in main.\t(tid is ", threads->tid, ")\n\n";
for(1..2){
    threads->new(sub{
        threads->new(sub{
            threads->new(sub{
                print '$CLONE is ', $CloneTest::CLONE,
                 " in thread.\t(tid is ", threads->tid, ")\n";
            });
            print '$CLONE is ', $CloneTest::CLONE,
             " in thread.\t(tid is ", threads->tid, ")\n";
        });
        print '$CLONE is ', $CloneTest::CLONE,
         " in thread.\t(tid is ", threads->tid, ")\n";
    });
    sleep(1);
    print "\n";
}
$_->join for(threads->list);
print '$CLONE is ', $CloneTest::CLONE,
        " in main.\t(tid is ", threads->tid, ")\n";
print "ok";
#---- 出力
>perl main.pl
$CLONE is 0 in main.    (tid is 0)

$CLONE is 1 in Module.  (tid is 0)
$CLONE is 2 in Module.  (tid is 1)
$CLONE is 1 in thread.  (tid is 1)
$CLONE is 3 in Module.  (tid is 2)
$CLONE is 2 in thread.  (tid is 2)
$CLONE is 3 in thread.  (tid is 3)

$CLONE is 1 in Module.  (tid is 0)
$CLONE is 2 in Module.  (tid is 4)
$CLONE is 1 in thread.  (tid is 4)
$CLONE is 3 in Module.  (tid is 5)
$CLONE is 2 in thread.  (tid is 5)
$CLONE is 3 in thread.  (tid is 6)

$CLONE is 0 in main.    (tid is 0)
ok
#--------------------------------------

 上の結果から、CLONEサブルーチン実行時のカレントスレッドは親スレッドであることがわかります。すなわち、threads->newで新しいスレッドが生成されると、その親スレッドのコンテキストでCLONEサブルーチンは実行されているようです(本当に新しいスレッドのコンテキストで何か初期化処理をしたいならば、Thread::Exitを使います)。また、ループで示されているように、$CloneTest::CLONEの値は親スレッドから引き継がれていることがわかります。

Perl5.8.1以降は正しくカレントスレッド内でCLONEが実行されます。

マルチスレッドにおける再現性のある乱数列

 5.8.0では親スレッドでperl組み込みのrand()を使うと、子スレッドで全て同じ乱数列が発生してしまう。この回避はsub CLONE{ srand() }を使うことだと、Thread::Randには書いてあります。実際に試して見ますと、v5.8.0 built for i386-linux-thread-multiではマルチスレッドを通じて毎回同じ乱数列が生成され、MSWin32-x86-multi-thread(activeperl 5.8.0)では各スレッド毎に同じ乱数列が発生しました。

 回避方法を試して見ますと、activeperl5.8.0ではメモリアクセス違反でperlが落ちました。i386-linux-thread-multiでは確かに回避されました。またactiveperl5.8.1以降ではこの問題が修正されていることを確認。i386-linux-thread-multiも5.8.3において修正を確認(他バージョンは未確認)。

 また、親スレッドでコアのsrand(一定値)を使っても、マルチスレッドを通じて再現性のある乱数列が発生しません。そこでThread::Randを使います(要Thread::Tie)。

#!/usr/bin/perl

use strict;
use warnings;
use threads;
use threads::shared;

BEGIN{
    use Thread::Rand;
    Thread::Rand->global;
}

srand(100);

my $count : shared;

for(1..3){
    threads->new(\&rand_test)->join;
}

sub rand_test{
    lock($count);
    printf("%.6f",rand(10), " ") for(1..5);
    print "\n";
}

 これで常に同じ乱数列が表示されます。Thread::Randを使わないと、毎回異なる乱数列になってしまいました(linux 5.8.1, linux 5.8.3, active 5.8.0-3 で確認)。

Thread::TieをWIN32 with activeperlで使う

 他の項でも書きましたが、ithreadにおける共有変数の正体はtieされた変数です。use threads::sharedすると暗黙の内に共有変数用のスレッドが生成されます。tieされた変数ですので、共有変数に対してはtieできません。そこでこれを可能にしようというのが、Elizabeth Mattijsen氏の実験的モジュールThread::Tieです。彼女の他の多くのモジュールがこれを利用して、メモリの消費を押さえたり、スレッド間でのデータのやりとりを行っています。

 ところがこれ、どうもwin32のactiveperlでは動かないようです。色々試してみたところ、原因は第一にThread::Tie::Thread内のサーバ・クライアント処理でデッドロックしてしまうのと、Thread::Serializeの後処理でメモリアクセス違反を起こしてしまうためではないかと思うのです(確証はありません)。で、デッドロックの方はT::T::Threadのコードを書き換えれば何とかなるのですが、問題はメモリアクセスの方でして、これはたぶんactiveperlが何とかならないとどうにもならないでしょう。ただ、それでも何とかT::T::Threadのコードを書き換えることでかなりまともに動かすことができそうです。誰もそんなの興味ないか……

 ActivePerl 5.8.4で、問題の原因であったStorableのバージョンが上がったため、メモリアクセス違反にはならなくなりました(まだデッドロックの問題が残っていますが)。

スレッドが生きているかどうかのチェック

 How to check if a thread is alive?より。

 dieしたスレッドがthreads->listのリストに入っているのはおかしいのでは?という質問。perlthrtutには「現在実行されていないか、detachされていないスレッド」のリストであると書いてあるところから。

 これに対する回答。threadsのドキュメントでは「joinもdetachもされていないスレッド」とより正確に書かれている。すなわち、dieしたスレッドは未だにjoinable。スレッドの生き死にを確認するにはThread::Runningを使うのが吉。

PAR(pp)とスレッド

 PARモジュール付属のppを使うと、perlスクリプトをスタンドアローンな実行形式にできます。ところが、use threadsしてスレッドを生成するようなコードをスタンドアローンにして実行すると、perlがクラッシュします。回避方法はどん底日誌のこの辺参照。

ithreads with mod_perl

 mod_perlとithreadsについて。とりあえずこの辺りですか? あと、このMLでのやりとりとか。

What is a "stash"?

 What is a "stash"?。ithreadsのチュートリアルに出てくる

When you return an object the entire stash that the object is blessed as well. This will lead to a large memory usage. The ideal situation would be to detect the original stash if it existed.

stashとは何かについて。っていうか、これはPerl内部の話か……

Scalars leaked

1. thread と referenceより:

 my $ref;
 test(\$ref);

 sub test {
 	my $thr = threads->new(sub {}, );
 	$thr->join();
 }

"Scalars leaked: 1"になる。たぶんサブルーチンtestの@_に外部から参照されていないリファレンスが残っていると、子スレッドにこれがコピーされて、threadsオブジェクトが破壊される際(サブルーチンの終わり部分)のガベージコレクトで悪さをしているのではないかと思います。そこで上記参照記事元にもあるように、my $ref_ref = \$refとして外部から参照させることでスレッドオブジェクトが破棄されてもガベージコレクトさせないか、あるいは単にサブルーチンの最初でshiftpopを使って@_から怪しげなものを取り除いてしまうと良いようです。

 my $ref;
 test(\$ref);

 sub test {
 	my $arg = shift;
 	my $thr = threads->new(sub {}, );
 	$thr->join();
 } # 今度は大丈夫

2. ithread 配下で Scalars leakedより:

 eval q{threads->new(sub{})->join};

"Scalars leaked: 1"になる話。

 my $str;
 eval q{threads->new(sub{ $str })->join};

とやると、何故かScalars leakedしない。our宣言された変数でもよい。ただしuse varsで宣言された変数はだめっぽい。一体どうなっているんだろう?

ページの先頭へ