スレッドを使用する主な理由は、プログラムのパフォーマンスを高めることにあります。スレッドの作成や管理には、オペレーティング・システムのオーバーヘッドをあまり伴わず、システム・リソースもそれほど消費しません。1 つのプロセス内のスレッドはすべて同じアドレス空間を共有するため、スレッド間で通信した方がプロセス間で通信するよりも効率的であり、また実装が容易です。例えば、あるスレッドが、I/O システム・コールの処理が完了するのを待っている場合、他のスレッドは CPU 負荷の重いタスクを実行することができます。また、スレッドを使用することで、重要なタスクを優先順位の低いタスクよりも優先してスケジューリングすることができ、さらには優先順位の低いタスクに割り込むことさえできます。頻度が少なく、散発的にしか行われないタスクは、定期的に実行されるようにスケジューリングされたタスクの合間に実行することができ、柔軟なスケジューリングが可能になります。こうしたスレッドの中でも pthread は、複数の CPU を持つマシンでの並列プログラミングにとって理想的です。
POSIX スレッド、つまり pthread を使用する主な理由はとても単純で、C 言語での標準化されたスレッド・プログラミング・インターフェースとして、pthread は極めて移植性が高いからです。
POSIX スレッド・プログラミングには多くのメリットがありますが、いくつかの基本的なルールを理解していないと、デバッグしにくいコードを作成してしまったり、メモリー・リークを発生させたりする危険性があります。ではまず、POSIX スレッドについて簡単におさらいすることから始めましょう。POSIX スレッドは、join 可能なスレッドか、または切り離されたスレッドのいずれかです。
新しいスレッドを作成したい場合で、そのスレッドがどのように終了するのかを知りたい場合には、join 可能なスレッドが必要です。join 可能なスレッドに対し、システムはスレッドの終了ステータスを保存するための専用ストレージを割り当てます。この終了ステータスはスレッドが終了すると更新されます。スレッドの終了ステータスを取得したい場合には、pthread_join(pthread_t thread, void** value_ptr) を呼び出します。
システムは各スレッドに対し、使用するストレージを割り当てます。割り当てるものとしては、スタック、スレッドの ID、スレッドの終了ステータスなどが含まれます。このストレージは、スレッドが終了して他のスレッドと join されるまで、そのプロセスの空間に残ります (そしてリサイクルはされません)。
ほとんどの場合は、単純にスレッドを作成し、そのスレッドに何らかのタスクを割り当て、それから他の処理を行います。こうした場合には、そのスレッドがどのように終了するかを気にする必要はないため、切り離されたスレッドは適切な選択肢です。
切り離されたスレッドの場合、そのスレッドが終了すると、システムはそのスレッドが使用していたリソースを自動的にリサイクルします。
join 可能なスレッドを作成したものの、そのスレッドを join するのを忘れてしまうと、そのスレッドのリソース、つまり専用メモリーは常にそのプロセスの空間に保持され、回収されません。join 可能なスレッドは、必ず join するようにして下さい。join しないでいると、深刻なメモリー・リークを生じる危険が出てきます。
例えば Red Hat Enterprise Linux (RHEL4) では、スレッドには 10MB のスタックが必要ですが、これはつまり、このスレッドを join しないと、少なくとも 10MB のメモリーがリークするということです。この RHEL4 の環境で、受信したリクエストを処理するためのプログラム (ワーカーを管理するプログラム) を設計するとします。このプログラムでは、ワーカー・スレッドを次々に作成して個々のタスクを実行させ、処理が完了したスレッドは終了する必要があります。これらのスレッドが join 可能なスレッドであるのに、スレッドを join するための pthread_join() を呼び出さなかったとすると、生成された各スレッドによって、そのスレッドの終了後、かなりの量のメモリー (スタックごとに少なくとも 10MB) がリークされることになります。さらにワーカー・スレッドが作成され、join されずに終了すると、メモリー・リークの量は増え続けます。やがて、新しいスレッドを作成するためのメモリーがなくなるため、このプロセスは新しいスレッドを作成できなくなります。
リスト 1 は、join 可能なスレッドを join するのを忘れると深刻なメモリー・リークが発生することを示しています。また、このコードを使うことで、1 つのプロセス空間で共存可能なスレッド本体の最大数をチェックすることもできます。
リスト 1. メモリー・リークを作り出す
#include<stdio.h>
#include<pthread.h>
void run() {
pthread_exit(0);
}
int main () {
pthread_t thread;
int rc;
long count = 0;
while(1) {
if(rc = pthread_create(&thread, 0, run, 0) ) {
printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
perror("Fail:");
return -1;
}
count++;
}
return 0;
}
|
リスト 1 では pthread_create() が呼び出され、デフォルトのスレッド属性が設定された新しいスレッドを作成しています。デフォルトで、新しいスレッドは join 可能です。pthread_create() によって新しい join 可能なスレッドが際限なく作成され、スレッドの作成に失敗するまでそれが続きます。そしてエラー・コードと失敗の理由が出力されます。
リスト 1 のコードを、[root@server ~]# cc -lpthread thread.c -o thread というコマンドを使って Red Hat Enterprise Linux Server リリース 5.4 でコンパイルすると、リスト 2 の結果が得られます。
リスト 2. メモリー・リークの結果
[root@server ~]# ./thread ERROR, rc is 12, so far 304 threads created Fail:: Cannot allocate memory |
リスト 1 のコードでは、304 本のスレッドを作成した後、さらにスレッドを作成することに失敗しています。エラー・コードの 12 は、もう使用できる空きメモリーがないことを意味しています。
リスト 1 と 2 に示したように、join 可能なスレッドが生成されていますが、それらのスレッドは決して join されません。そのため、join 可能な各スレッドは終了しても相変わらずプロセスの空間を占有しており、プロセスのメモリー・リークを引き起こしています。
RHEL の POSIX スレッドには、サイズが 10MB の専用スタックがあります。つまり、システムは各 pthread に少なくとも 10MB の専用ストレージを割り当てます。この記事の例では、304 本のスレッドが生成された後、プロセスが停止しています。これらのスレッドは 304 x 10MB のメモリー、つまり約 3GB を占有します。1 つのプロセスの仮想メモリーのサイズは 4GB であり、プロセス空間の 1/4 が Linux カーネル用に予約されています。合計すると、ユーザー空間として 3GB のメモリー空間が得られます。つまり使われなくなったスレッドによって 3GB のメモリーが消費されています。これは深刻なメモリー・リークです。そして、こんなにも早くメモリー・リークが発生した様子を簡単に見て取れます。
メモリー・リークを修正するためには、join 可能な各スレッドを join するためのコードを pthread_join() への呼び出しに追加します。
他のメモリー・リークの場合と同様、このメモリー・リークの問題も、プロセスが起動された時点では明らかでないかもしれません。そこで、そうした問題があることを、ソース・コードを見ずに検出する方法を以下に示します。
- プロセスの中のスレッド・スタックの数を数えます。この数には、実行中のアクティブなスレッドと、終了したスレッドの数が含まれています。
- プロセスの中で実行中のアクティブなスレッドの数を数えます。
- その 2 つの数を比較します。プログラムの実行中、既存のスレッド・スタックの数の方が実行中のアクティブなスレッドの数よりも多く、2 つの数の差が広がり続けている場合には、メモリー・リークが発生しています。
そして、ほぼ間違いなく、そうしたメモリー・リークは join 可能なスレッドを joinしていないために発生しています。
実行中のプロセスでは、スレッド・スタックの数はプロセスの中にあるスレッド本体の数と同じです。スレッド本体を構成しているのは、アクティブに実行中のスレッドと、使われなくなった join 可能なスレッドです。
pmap はプロセス・メモリーに関してレポートするための Linux ツールです。下記のコマンドを組み合わせると、スレッド・スタックの数を得ることができます。
[root@server ~]# pmap PID | grep 10240 | wc -l
(10240KB は Red Hat Enterprise Linux Server リリース 5.4 のデフォルトのスタック・サイズです)。
/proc/PID/task を使ってアクティブなスレッドを数える
スレッドが作成されて実行されるたびに、/proc/PID/task にエントリーが追加されます。スレッドが終了すると、そのスレッドが join 可能なスレッドであれ、または切り離されたスレッドであれ、そのスレッドのエントリーは /proc/PID/task から削除されます。つまり下記を実行すると、アクティブなスレッドの数を得ることができます。
[root@server ~]# ls /proc/PID/task | wc -l
pmap PID | grep 10240 | wc -l を実行した場合の出力を調べ、ls /proc/PID/task | wc -l の出力と比較します。プログラムの実行中、すべてのスレッド・スタックの総数の方がアクティブなスレッドの数よりも多く、その差が広がり続けている場合には、リークの問題が存在していると結論付けることができます。
プログラミングを行う際には、join 可能なスレッドを join する必要があります。join 可能なスレッドをプログラムの中で作成する場合には、忘れずに pthread_join(pthread_t, void**) を呼び出し、スレッドに割り当てられた専用ストレージをリサイクルします。そうしないと、深刻なメモリー・リークが発生します。
プログラミングが完了した後、テスト・フェーズでは、メモリー・リークが存在するかどうかを、pmap と /proc/PID/task を使って検出することができます。リークが存在する場合には、ソース・コードを調べ、join 可能なスレッドがすべて join されているかどうかを確認します。
そして、それがすべてです。少し注意するだけで、後の作業を減らすことができ、またやっかいなメモリー・リークを防ぐことができます。
学ぶために
- 「/procファイルシステムを使用したLinuxカーネルへのアクセス」(developerWorks、2006年3月) は、この仮想ファイルシステムのユニークな通信機能を最大限に活用する方法を紹介しています。
- また、Terrehon Bowden と Bodo Bauer による、/proc ファイルシステムのマニュアルを読んでください。
- pthread についてさらに学びたい人のために、Mark Hays による POSIX Threads Tutorial は、POSIX スレッドを使って並列アプリケーションを作成する方法を説明しています。
- linuxmanpages の
pmapのセクションでは、このマッピング・コマンドに関して必要な事項をすべて説明しています。 - developerWorks の Linux ゾーンには、何百という記事やチュートリアル、またダウンロード、ディスカッション・フォーラム、その他さまざまなリソースが Linux 開発者や管理者のために用意されています。
- さまざまな IBM 製品や IT 業界の話題に焦点を絞った developerWorks の Technical events and webcasts で最新情報を入手してください。
- 無料の developerWorks Live! ブリーフィングに出席し、IBM の製品やツール、また IT 業界のトレンドに関する最新情報を入手してください。
- developerWorks On demand demos をご覧ください。初心者のための製品インストール方法やセットアップのデモから、上級開発者のための高度な機能に至るまで、多様な話題が解説されています。
- developerWorks を Twitter でフォローするか、あるいは Linux に関する developerWorksのツィートのフィードを購読してください。
製品や技術を入手するために
- 皆さんの目的に最適な方法で IBM 製品を評価してください。製品の試用版をダウンロードする方法、オンラインで製品を試す方法、クラウド環境で製品を使う方法、あるいは SOA Sandbox で数時間を費やし、サービス指向アーキテクチャーの効率的な実装方法を学ぶ方法などがあります。
議論するために
- My developerWorks コミュニティーに参加してください。そして開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。
