POSIX スレッド・プログラミングでのメモリー・リークを防ぐ

POSIX スレッドのメモリー・リークを検出し、回避するためのヒント

POSIX スレッド (pthread) プログラミングでは、C プログラミング言語で標準となっている一連の型、関数、定数を定義します。また pthread はスレッドを管理するための強力なツールです。pthread を最大限に活用するためには、よくやってしまいがちなミスをしないようにする必要があります。よくやりがちなミスの 1 つが、join 可能なスレッドを join するのを忘れてしまうことです。これを忘れるとメモリー・リークが発生し、余計な作業が増えることになります。助言を与えることを目的としたこの記事では、POSIX スレッドの基本を学び、スレッドのメモリー・リークがあることを特定して検出する方法を調べ、最後にメモリー・リークを回避するための確実なアドバイスを提供します。

Wei Dong Xie, IBM Systems Director Product Engineer, IBM

Author photo of Wei Dong XieWei Dong は過去 3 年間、IBM Systems Director の製品エンジニアとして、顧客から報告された問題の修復に従事してきました。IBM に入社前には、Intel で 10 ヶ月間、インターンの Linux 開発者として働いていました。彼は 2007年に中国の南京大学を修士で卒業しています。



2010年 8月 25日

POSIX スレッドの紹介

スレッドを使用する主な理由は、プログラムのパフォーマンスを高めることにあります。スレッドの作成や管理には、オペレーティング・システムのオーバーヘッドをあまり伴わず、システム・リソースもそれほど消費しません。1 つのプロセス内のスレッドはすべて同じアドレス空間を共有するため、スレッド間で通信した方がプロセス間で通信するよりも効率的であり、また実装が容易です。例えば、あるスレッドが、I/O システム・コールの処理が完了するのを待っている場合、他のスレッドは CPU 負荷の重いタスクを実行することができます。また、スレッドを使用することで、重要なタスクを優先順位の低いタスクよりも優先してスケジューリングすることができ、さらには優先順位の低いタスクに割り込むことさえできます。頻度が少なく、散発的にしか行われないタスクは、定期的に実行されるようにスケジューリングされたタスクの合間に実行することができ、柔軟なスケジューリングが可能になります。こうしたスレッドの中でも pthread は、複数の CPU を持つマシンでの並列プログラミングにとって理想的です。

POSIX スレッド、つまり pthread を使用する主な理由はとても単純で、C 言語での標準化されたスレッド・プログラミング・インターフェースとして、pthread は極めて移植性が高いからです。

POSIX スレッド・プログラミングには多くのメリットがありますが、いくつかの基本的なルールを理解していないと、デバッグしにくいコードを作成してしまったり、メモリー・リークを発生させたりする危険性があります。ではまず、POSIX スレッドについて簡単におさらいすることから始めましょう。POSIX スレッドは、join 可能なスレッドか、または切り離されたスレッドのいずれかです。

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() への呼び出しに追加します。


メモリー・リークを検出する

他のメモリー・リークの場合と同様、このメモリー・リークの問題も、プロセスが起動された時点では明らかでないかもしれません。そこで、そうした問題があることを、ソース・コードを見ずに検出する方法を以下に示します。

  1. プロセスの中のスレッド・スタックの数を数えます。この数には、実行中のアクティブなスレッドと、終了したスレッドの数が含まれています。
  2. プロセスの中で実行中のアクティブなスレッドの数を数えます。
  3. その 2 つの数を比較します。プログラムの実行中、既存のスレッド・スタックの数の方が実行中のアクティブなスレッドの数よりも多く、2 つの数の差が広がり続けている場合には、メモリー・リークが発生しています。

そして、ほぼ間違いなく、そうしたメモリー・リークは join 可能なスレッドを joinしていないために発生しています。

pmap を使ってスレッド・スタックを数える

実行中のプロセスでは、スレッド・スタックの数はプロセスの中にあるスレッド本体の数と同じです。スレッド本体を構成しているのは、アクティブに実行中のスレッドと、使われなくなった 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 されているかどうかを確認します。

そして、それがすべてです。少し注意するだけで、後の作業を減らすことができ、またやっかいなメモリー・リークを防ぐことができます。

参考文献

学ぶために

製品や技術を入手するために

  • 皆さんの目的に最適な方法で IBM 製品を評価してください。製品の試用版をダウンロードする方法、オンラインで製品を試す方法、クラウド環境で製品を使う方法、あるいは SOA Sandbox で数時間を費やし、サービス指向アーキテクチャーの効率的な実装方法を学ぶ方法などがあります。

議論するために

  • My developerWorks コミュニティーに参加してください。そして開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=548870
ArticleTitle=POSIX スレッド・プログラミングでのメモリー・リークを防ぐ
publish-date=08252010