トップ回答者
DLL_THREAD_ATTACHで割り当たTLSをDLL_PROCESS_DETACHで解放するには?

質問
-
http://msdn.microsoft.com/ja-jp/library/cc429094.aspx
に以下の記述があります。
> DLL のロードに失敗した結果、その DLL がプロセスからアンロードされた場合、そのプロセスを終了する際、または FreeLibrary を呼び出す際に、システムはプロセスの個別のスレッドで、DLL_THREAD_DETACH を指定して DLL のエントリポイント関数を呼び出すことはありません。システムは、その DLL へ DLL_PROCESS_DETACH の通知を送信するだけです。DLL はこの機会を利用して、DLL が把握しているすべてのスレッドのすべてのリソースをクリーンアップ(解放)できます。
※太字部分は以下を参照して誤植を訂正してあります。
http://msdn.microsoft.com/en-us/library/windows/desktop/ms682583(v=vs.85).aspx
DLL_PROCESS_DETACHでメインスレッドのTLSの解放とTlsFreeができるのはわかりますが、このときにDLL_THREAD_DETACHが実行されていないスレッドの未解放のTLSも解放する(ためにTlsGetValueで未解放のポインタを取得する)にはどうすればよいのでしょうか?
回答
すべての返信
-
日本語訳だと「DLLのロードに失敗した結果」がどこまでに掛かっているか曖昧ですが、原文をみると
- DLL のロードに失敗した結果、その DLL がプロセスからアンロードされた場合
- そのプロセスを終了する際
- FreeLibrary を呼び出す際
ということだと思われます。(英語は苦手なので間違っているかもしれませんが…)
なので、質問はDLLがロードできており、DLL_THREAD_ATTACHも実行されている状況に関してです。
もともとこの質問に至った経緯としては、実際に以下の現象に遭遇したためです。
- C++/CLI(or Managed C++)コンソールからUnmanaged C++ DLLの関数を呼び出した場合
以下はコンソールへのログ出力を仕込んだ実験プログラムを VS2003 で作成し、実行後約40分放置してから終了したときログです。(MC=Managed C++、UC=Unmanaged C++)
UC> TID=1588, DLL_PROCESS_ATTACH : 0 sec
UC> TID=4196, DLL_THREAD_ATTACH : 0.068 sec
UC> TID=2544, DLL_THREAD_ATTACH : 0.085 sec
MC> Test start.
UC> Unmanaged Test() was called.
MC> Waiting for the enter key.
(Enter)
MC> Test end.
UC> TID=1588, DLL_PROCESS_DETACH : 2480.26 sec - C#コンソール → C++/CLI(or Managed C++) DLL → Unmanaged C++ DLL の場合
同様の実験プログラムを VS2003 で作成し、実行後約40分放置してから終了したときログです。(UC=Unmanaged C++)
UC> TID=5856, DLL_PROCESS_ATTACH : 0 sec
C#> Test start.
UC> Unmanaged Test() was called.
C#> Waiting for the enter key.
UC> TID=6040, DLL_THREAD_DETACH : 97.419 sec
UC> TID=6060, DLL_THREAD_ATTACH : 386.078 sec
UC> TID=3240, DLL_THREAD_DETACH : 754.553 sec
(Enter)
C#> Test end.
UC> TID=5856, DLL_PROCESS_DETACH : 2481.64 sec
※時間はDLL_PROCESS_ATTACHからの経過時間です。
※内部的にはプロセスを終了しようとしたときにCLRがFreeLibraryを呼んでいるのかもしれません。
※C#でUnmanaged C++ DLLを直接DLL Importした場合はDLL_THREAD_ATTACH、DLL_THREAD_DETACHは起こりませんでした。
※環境はWinXP SP3+VS2003で、現時点でのセキュリティパッチ類は一通り適用済です。VS2005でもほぼ同様でした。
-
TLS を使ったことが無いので的外れかもしれませんが、 TLS API は TLS インデックスでスロットを管理する構造のようなのでメインスレッドから普通に扱える気がします。
説明をみても TLS インデックスは静的領域に保存するような記述に見えますので、 仲澤@失業者さんの言われているように DLL_PROCESS_DETACH で未開放のメモリを解放すれば良いだけに思えますね。
本題については以上なのですが、質問に至った経緯を見る限り、DllMain が DLL_THREAD_ATTACH と DLL_THREAD_DETACH で呼出される条件のために混乱しているような気がします。
DLL_THREAD_ATTACH は DLL が読込まれた後に Thread が作成された時に発生し、DLL_THREAD_DETACH は Thread が終了しプロセスから解放された時に発生するといった記述になっているので、経緯のような状況はコード次第ではありますが普通に可能性があると思いますよ。
C# から DLLImport 属性を使ってテストした時に起こらないというのは、ロード後に Thread を作成&破棄が発生してないのではないですかね?
-
DLL_THREAD_ATTACHやDLL_THREAD_DETACHが呼ばれないことがある点についての質問だったのですね。この動作については知りませんでした。
質問からはそれますが、.NETが出てきたので念のため指摘しておきます。Microsoft Windows でのマネージ スレッド処理とアンマネージ スレッド処理に
アンマネージ ホストでマネージ スレッドとアンマネージ スレッドとの関係を制御できるため、オペレーティング システムの ThreadId とマネージ スレッドを固定的に関係付ける必要はありません。 つまり、高度なホストはファイバー API を使用することによって、多数のマネージ スレッドを同じオペレーティング システムのスレッドにスケジュールしたり、マネージ スレッドを異なるオペレーティング システム スレッド間で移動したりできます。
とあります。「必要はありません」と訳されていますが原文は「An operating-system ThreadId has no fixed relationship to a managed thread」なので「固定的な関係はありません」ぐらいかな。
つまりマネージスレッドはどのネイティブスレッドで動作するのかは固定されていません。マネージスレッドからアンマネージDLLを呼び出した場合についてもマネージスレッドが同じであってもそこで使用されるネイティブスレッドが何になるかは不定というわけです。
このような前提があると、TLSを使うのは非常に困難ではありませんか?現実的にはファイバーAPIを使用する計画はあったんだそうですが、いろいろ技術的に困難で見送られています。とはいえ、.NET 4ではTaskクラスの登場などでマネージスレッドとネイティブスレッドの関係が崩れつつあるように思います。
-
DLLのスタックはそれを呼び出したプロセスのものなので、
各スレッド起動時に確保したメモリのハンドルなり、ポインタなりは、
はスタックに記録しておけます。DLLのスタックはWindows の限界に挑む: プロセスとスレッドの「スレッドの制限」で説明されているスレッドスタックのことを指していると解釈しました。
だとして、DLL_PROCESS_DETACH時に別スレッドのスレッドスタックを参照することは可能なのでしょうか?
というか、スタックは自動変数などの保存先なので今回の目的には使えない気がします。グローバル変数ないし静的変数の間違いでしょうか?
-
説明をみても TLS インデックスは静的領域に保存するような記述に見えますので、 仲澤@失業者さんの言われているように DLL_PROCESS_DETACH で未開放のメモリを解放すれば良いだけに思えますね。
通常TLSインデックスはグローバル変数として保存し、TLS APIはTLSインデックスとスレッドIDに基づいてデータを格納するようです。
基本的なサンプルコードは例えばUsing Thread Local Storage in a Dynamic-Link Library Program Exampleのようになるかと思います。
DLL_PROCESS_DETACHで別スレッドがTLSに格納したデータを解放には、グローバル変数でも自前で管理しておかなければならないような気がしてきました。
DLL_THREAD_ATTACH は DLL が読込まれた後に Thread が作成された時に発生し、DLL_THREAD_DETACH は Thread が終了しプロセスから解放された時に発生するといった記述になっているので、経緯のような状況はコード次第ではありますが普通に可能性があると思いますよ。
はい。そのようなことが起こりえることは理解しています。解決したいのはそのときのDLL_PROCESS_DETACHでクリーンアップを漏れなく行う方法です。
C# から DLLImport 属性を使ってテストした時に起こらないというのは、ロード後に Thread を作成&破棄が発生してないのではないですかね?
そうですね。DLLImportの場合は呼びだし元(C#)の最初のログ出力のあとにDLL_PROCESS_ATTACHが実行されており、前述の2ケースとはDLLをロードする仕組みが異なるようです。その関係でCLRが作成するスレッドもタイミングや数が異なるのだと思われます。
C#> Test start.
UC> TID=4884, DLL_PROCESS_ATTACH : 0 sec
UC> Unmanaged Test() was called.
C#> Waiting for the enter key.
(Enter)
C#> Test end.
UC> TID=4884, DLL_PROCESS_DETACH : 2479.93 secこのような前提があると、TLSを使うのは非常に困難ではありませんか?
なるほど。マネージコードからTLSを利用するDLLを呼び出すには注意が必要そうですね(そもそもやってはいけないことなのかもしれません)。
ただ、例に挙げたマネージコードからの呼び出しだけでなく、一般的にLoadLibrary/FreeLibraryを利用するマルチスレッドプログラミングでは同じことが起きるかと思います。
なので、最初に引用したヘルプにも
DLL はこの機会を利用して、DLL が把握しているすべてのスレッドのすべてのリソースをクリーンアップ(解放)できます。
とあるのだと思います。
検索すると上記サンプルコードの他にも似たようなコードは見つかりますが、ここまで考慮している物は見つけられませんでした。
質問に至った経緯はさておき、そもそも別スレッドがTLSに登録したデータをDLL_PROCESS_DETACHで解放するにはどうすればよいかについて、引き続きご助言頂ければと思います。
-
>質問に至った経緯はさておき、そもそも別スレッドがTLSに登録したデータをDLL_PROCESS_DETACHで解放するにはどうすればよいかについて、引き続きご助言頂ければと思います。
当たり前ですが、そもそもDLL内における、TLSの利便性とは、
DLL_THREAD_ATTACHとDLL_THREAD_DETACHが確実に実行されることを
前提としています。これを疑う場合は当初からTLSを使用すべきでは
ないと考えられます。
一般にTLS相当のメモリー制御、又はスレッド関数への再入時毎
のメモリー制御は簡単にコードできますので、心配なら
自前でコードすればよいわけですね。 -
当たり前ですが、そもそもDLL内における、TLSの利便性とは、
DLL_THREAD_ATTACHとDLL_THREAD_DETACHが確実に実行されることを
前提としています。これを疑う場合は当初からTLSを使用すべきでは
ないと考えられます。DllMainを読む限りそのような前提は一般的に成り立たない(DLLの利用の仕方に依存する)ので、そのDLLを不特定多数のエンドユーザーが利用するような場合にはTLSはあまり適していないということですね。
それでもTLSを使いつつ解決するとすれば、仲澤さんが最初に書かれたようにグローバル変数に別途記憶しておくのが良さそうです。
- DLL_THREAD_ATTACHでTLSに格納したポインタをグローバルなリストにも別途格納しておき、DLL_THREAD_DETACHではリストから削除する。
DLL_PROCESS_DETACHの際にリストに残エントリーがあれば解放する。(TLS相当のメモリー制御をすべて自前で書くよりは楽そう)
という感じでしょうか。
- DLL_THREAD_ATTACHでTLSに格納したポインタをグローバルなリストにも別途格納しておき、DLL_THREAD_DETACHではリストから削除する。
-
ご存知かもしれませんが念のため、
__declspec( thread )によりTlsAlloc()に依らないスレッドローカルストレージが実現できます。ただし使用にはいろいろと制限が付きます。とはいえWindows Vista以降では一部の制限が解除されています。
-
経験では DLL_THREAD_ATTACH と DLL_THREAD_DETACH が、対になることを前提としてはいけないと思っていたのでヘルプ記述に納得していました。
ヘルプ記述は DLL 外で管理されるスレッドに対して TLS の確保&破棄を行うために DllMain 呼出しを利用することができるというだけの話ではないでしょうか?
日本語訳のヘルプ誤植のせいで混乱させられますが、 DLL_PROCESS_DETACH が「すべてのリソースをクリーンアップ」 する機会と記述されてますので・・・。
DLL_THREAD_ATTACHでTLSに格納したポインタをグローバルなリストにも別途格納しておき、DLL_THREAD_DETACHではリストから削除する。
- DLL_PROCESS_DETACHの際にリストに残エントリーがあれば解放する。(TLS相当のメモリー制御をすべて自前で書くよりは楽そう)
という感じでしょうか。
TLS の経験はないので説得力ないですが、プロセス内のメモリ空間(リソース)はスレッド間で共有されていることを考えれば、そのようにするのが順当だと思いますよ。
「DLLの管理によるスレッド間でメモリ分離するべき状況」というのは、COMサーバー等の「不特定多数のエンドユーザー」がスレッド間での利用をする場合くらいだろうと思っていたのですが、一般的にどういう状況で利用されるものなのか気になりますね・・・。
-
__declspec( thread )によりTlsAlloc()に依らないスレッドローカルストレージが実現できます。ただし使用にはいろいろと制限が付きます。とはいえWindows Vista以降では一部の制限が解除されています。
ありがとうございます。
ただ、XPもサポートする必要があるため、残念ながら__declspec( thread )は使用できないのです…
-
「DLLの管理によるスレッド間でメモリ分離するべき状況」というのは、COMサーバー等の「不特定多数のエンドユーザー」がスレッド間での利用をする場合くらいだろうと思っていたのですが、一般的にどういう状況で利用されるものなのか気になりますね・・・。
もともと私が作成した物ではありませんが、修正したいDLLでは独自のエラー情報(エラーコード、エラーメッセージ、他)の保存先としてTLSが利用されていますね。
ちなみに、質問に至った経緯であるマネージコードからの利用ですが、このDLLの場合は運悪くエラー情報が取れないことがあったとしても致命的ではないので、あまり気にしなくても大丈夫そうです。厳密にやるとしたら、例えば戻り値がNGの時にエラー情報(のコピー)をOUT引数で返せるようなUnmanageのラッパーを1枚被せればよいですね。
最初の質問が解決しましたので、これにてクローズしたいと思います。
ご助言頂いた皆様、どうもありがとうございました。