2010-10-14

MSVCのランタイムとスレッドとリソースリークの関係

本の虫: いまだに変な宗教が流行っているを書いたところ、どうもこのへんの情報は、あまり知られていないようであるので、できるだけ分かりやすく解説することにした。

Cの標準ライブラリは、恐ろしく古いライブラリである。その設計は、マルチプロセッサ(コア)上で動作するマルチスレッドが当然の現代では、あまりよろしくない。

たとえば、strtokという関数がある。この関数は、引数として渡された文字列を、内部のバッファにコピーする。次のstrtokの引数には、NULLを渡すことで、そのコピーされたバッファから、次のトークンの場所へのポインターを返すのである。

void f( char const * ptr )
{
    char const * p1 = strtok( ptr, " " ) ;
    char const * p2 = strtok( NULL, " " ) ;
}

しかしもし、strtokが複数のスレッドから呼ばれた場合、どうなるのだろうか。引数にNULLを渡して、先程の文字列のコピーから、次のトークンの場所を返してくれることを期待しているのに、別のスレッドによって、勝手に内部の文字列が書き換わってしまっては困る。

この、複数のスレッドから利用しても大丈夫かどうか、ということは、非常に重要である。これをスレッドセーフと呼んでいる。

したがって、あるスレッドがstrtokを呼んでも、別のスレッドの内部バッファまで変更しないように実装しなければならない。これには、スレッドごとに、メモリーを確保する必要がある。

Cの標準ライブラリには、このようなスレッドアンセーフな設計のライブラリが山ほどある。Cの標準ライブラリは、まともなC++プログラマーならば、まず使おうとは思わないだろう。

言うまでもなく、このような設計は、現代ではクソである。当時のプログラマーを問い詰めたいところなのだが、まあ、すでになされたものは仕方がない。今さら互換性を失うわけにもいかないのだ。クソなことにはかわりはないが。

さて、このような、スレッドごとの動的に確保されるメモリーのアドレスを保持しておくための変数が必要になる。Windowsでは、TLS(Thread Local Storage)という仕組みが、OSによって用意されている。これにアドレスを格納しておけばいいのだ。

Windowsには、CreateThreadという、スレッド作成用のAPIが存在する。しかし、MSVCでは、独自のスレッド作成用の関数、__beginthreadexを使用することが推奨されている。これはなぜかというと、標準ライブラリに必要な、スレッドごとのメモリを確保、解放するためである。

では、もしMSVC上で、CreateThreadを用いてスレッドを作成したら、どうなるのか。スレッドごとの動的に確保されたメモリーを必要とする、一部の標準ライブラリは、動かなくなるのだろうか。幸い、その心配はない。MSVCの標準ライブラリの実装は、スレッドごとの動的なメモリーを必要とする関数が呼ばれたときに、TLSを確認し、もし、メモリーが確保されていなければ、その場で確保して、何事もなかったかのように処理を続ける。これによって、「動かない」という最悪の自体を回避できる。

しかし、ここでひとつ、問題がある。その確保したメモリーは、いつ解放すればいいのだろうか。もちろん、スレッドが終了するとき、すなわち、スレッドというリソースが解放される時である。しかし、スレッドの終了をどうやって補足すればいいのか。スレッドの終了を補足する方法は、なかなかにややこしい。

スレッドが、プロセスの終了まで、ずっと生存しているのならば、問題はない。Windowsにおいては、プロセスの所有するリソースは、プロセスというリソースの解放に伴い、自動的に解放されるからだ。プロセスが解放されたとき、後には何も残さない。もちろん、他のプロセスがプロセスハンドルを掴んでいる場合は、プロセスハンドルを維持するためのリソースは生き残っているが、それはまた別の話である。プロセスのスレッドはすべて開放されているが、まだプロセスが開放されていない状態である。

問題は、CreateThreadによるスレッドの生成、一部の標準ライブラリの仕様、スレッドの破棄を繰り返すプログラムである。そのようなプログラムでは、スレッドを生成、破棄するたびに、解放できないメモリーが取り残される。すなわち、リソースリークである。

実は、DLLを使えば、このスレッドの終了を補足するのは、非常に簡単である。なぜならば、プロセスに読み込まれている、すべてのDLLのDllMain関数は、プロセスのあらゆるスレッドの終了時に、DLL_THREAD_DETACH通知を受ける。この通知を使えば、TLSに保存されているアドレスのメモリーを解放できるのだ。

つまり、DLL版のランタイムライブラリを使えば、CreateThreadやThread Poolによって生成されたスレッドから、一部の標準ライブラリを使っても、リソースはリークしない。スレッド終了時に、必ず解放される。しかし、スタティックリンク版は、この方法が使えない。そして、MSVCの実装も、何とかするようには出来ていない。結果、リソースリークする。

そもそも、他ならぬマイクロソフト自身が、スタティックリンク版を使うなと明言したのが、確かVS2005の頃だった。スタティックリンク版のCRT、ANSI版のAPI、Unicode以外の文字コード、Windows XP、x86の32bitコード、IBM PCのBIOS、これらは皆、速やかに滅ぶべきものどもである。いやしくもプログラマーたるものは、実装の美しさを尊ばねばならぬ。特に、64bitコードの関数の呼び出し規約の統一は、涙が出るほどありがたいはずだ。それにも関わらず、旧態依然の古めかしい知識と技術に固執する。愚かなること、これに過ぎたるはなし。

追記:はてなブックマークのコメントより、

前半は良いのだが、後半のCreateThread()でのメモリリークについては、ちょっと最新の仕様とあってない気がする。Windowsは途中からスレッドハンドルをクローズしなくてはならない仕様になった

これはCRTとは何の関係もない話である。これはWin32 APIの仕様だ。この記事は、Windows、MSVC環境下における、スレッドとCRTのリソースリークについて書いているので、Win 32 APIの仕様には特に触れなかった。

ただし、これもスレッドとリークに関連はある。もちろん、CreateThreadで得たスレッドハンドルは、CloseHandleによって解放しなければならない。なぜならば、CloseHandleを呼び出さなければ、まだスレッドハンドルを、そのプロセスで使っているということだからだ。もちろんこれは、CreateProcessにおけるプロセスハンドルにも言えることだ。してみれば、Win32 API全般に関する注意点であって、特にスレッドに限定される話ではない。

No comments: