トップ 検索 一覧 差分 ソース ヘルプ RSS ログイン

mainとスレッドの前後処理(2)

この記事はmainとスレッドの前後処理(1)の続きです。次の記事はmainとスレッドの前後処理(3)です。

この記事は早くも古いものとなりました。詳細はMinGW+pthread2010.03?を参照。

mainとスレッドの前後処理(2)

前記事では、プロセスの開始・終了時に処理を割り込ませる方法に関して、大雑把に見当をつけた。続くこの記事では、スレッドの開始・終了時に処理を割り込ませる方法に関して探っていく。

 スレッドの開始・終了

pthread-w32自体がスレッド実装のライブラリだというのに、それを使う上でスレッドの開始・終了時に何かしなければならないというのは本末転倒な気がする。

第一、スレッドの開始時にはスレッドが処理する関数を関数ポインタで、そしてユーザデータをvoid*で渡すのが一般的なのだから、これをpthread-w32側でラップすればいいじゃないか。…と思ったら、そういえばスレッド開始時に呼ぶべき関数はREADMEで以下のように書かれていた。

BOOL pthread_win32_thread_attach_np (void); // Currently a no-op

実装も、

BOOL

pthread_win32_thread_attach_np ()

{

return TRUE;

}

となっているので、ここは上手くやってくれるようになっている、あるいは対応自体が必要ないらしい。

となると、問題はスレッドの終了時だ。こちらも、スレッドが必ずスタートした関数からreturnで返るなら隠蔽できるようにも思う。しかしこちらはpthread-w32の中でどうにかするようにはなっていないようだ。

どうにかしてくれないのなら自分でどうにかするしかないが、こちらはプロセスのようにはいかない。グローバル変数・スタティック変数でプロセス終了時にデストラクタが呼ばれるような仕組みは、スレッドには期待できない。そもそもスレッド自体、C++0xより前の標準C/C++では扱っていない概念なので、その枠内ではどうしようもない。いよいよ実装依存の機能でしか触れられない範疇になる。

その前に、「スレッド開始・終了時のフック的処理タイミングが一般的に処理系に用意されるべきか」という点について、少し述べておこう。

標準Cライブラリにはstrtok()という関数がある。C言語を少しかじっていれば常識だが、これはリエントラント(再入)不可の関数だ。strtok()はその仕様からわかる通り、内部に状態を持っている。これを複数のスレッドから呼び出すと、内部状態の整合が取れず、滅茶苦茶な処理を行うことになってしまう。こういう、複数スレッドから呼ばれると問題のある関数をリエントラント不可であるという。

この問題を避けるためには、strtok()がスレッドごとに内部状態を持てば良い。多くの処理系ではそのように実装したstrtok_r()が用意されている。このようなスレッドごとの記憶領域を、一般にThread Local Storage(TLS)と呼ぶ。この領域を確保・初期化するため、スレッドでC/C++の標準ライブラリを使う場合には、スレッドを_beginthread等の非標準の関数で作成する必要がある。

今回の経緯であるpthread-w32でスレッド開始・終了時に特定関数を呼ぶ必要があるというのは、つまり_beginthreadと同様にpthread用のTLSを初期化・解放するためと思っていいだろう。TLS自体はGCCでは__threadで、VC++では__declspec(thread)で簡単に確保できるようになっている。pthreadの話から離れて、これら以外のコンパイラでTLSを確保したい場合には、Win32APIのTlsAlloc関数等で機能自体は実現可能だ。しかしCreateThread 関数_beginthread, _beginthreadexもスレッドの開始関数にパラメータを渡せるのだし、スレッド側でもメモリ確保は可能なので、TLS自体はそれほど必須の機能でもない。TLSがどうしても必要なのはstrtokのように設計が悪いからだと思った方が良い。

今回のpthread-w32の件ではユーザコードに手を加えることなく、スレッドの開始・終了時に処理を呼び出したいのであり、これ自体は明示的なTLSの仕組みの有無とは無関係だ。処理系としても関数を呼び出せる仕組みがあった方が柔軟であるのは間違いない。逆に言えばスレッドの開始・終了時のフックができるのなら、TLSはその仕組みの中でどうにかすることもできる。

スレッドの開始・終了時処理の実装

Windowsは、スレッドの開始・終了時に特定の処理を行う窓口として、TLS-Callbackという仕組みを持っている。文字通り、本来はTLSの初期化や解放処理のためのコールバック関数が使用できる仕組みだ。しかし、これに関しては公式のドキュメントが非常に少ない。辛うじてInside Windows: An In-Depth Look into the Win32 Portable Executable File Format, Part 2という記事があり、そこでわかるのは以下のようなことだ。

  • __declspec(thread)によるTLSの初期化に使われる値は.tlsセクションに置かれる。
  • PEにはインポート・エクスポートテーブル等を記述するDataDirectoryと呼ばれる領域がある。
  • DataDirectoryの一部であるIMAGE_DIRECTORY_ENTRY_TLSが非ゼロならIMAGE_TLS_DIRECTORYのアドレスを指している。
  • TLS-Callbackに関するキモはこのIMAGE_TLS_DIRECTORY構造体にある。
  • IMAGE_TLS_DIRECTORY構造体は.tlsではなく.rdataセクションに置かれる。
  • IMAGE_TLS_DIRECTORYにコールバック関数群を指すポインタ配列のアドレスを設定する。
  • コールバック関数はPIMAGE_TLS_CALLBACKという型である。

これで緒は掴めたが、具体的な実装方法や注意事項がよくわからない。IMAGE_TLS_DIRECTORYで検索してみると、MSDNのDUMPBINコマンドのドキュメントに/TLS (C++)という項目が見つかる。これによるとIMAGE_TLS_DIRECTORYはwinnt.hで定義されるとのこと。つまり、Windows9xではTLS-Callbackは使えない。これは結構重要な注意事項だ。

このことはTLS Callbacks - NyaRuRuの日記TLS Callbacksでも言及されている。極最近(2009/12/10)MLにpostされたNabble - MinGW - Dev - TLS Callback SupportによればMinGWでもTLS-Callbackのサポートがmingwrtに追加されたようだが、これもSourceForgeのTrackerコメントのMinGW - Minimalist GNU for Windows: Detail: 2912598 - TLS callback supportを見ると、Win9x/Meのサポートはmingwm10.dllによるものとなっている。また、このMinGWの対応ではgcc/g++の4.3.4以降が必要になりそうな点も注意が必要だ。

 理屈の整理

話が枝葉末節に飛びそうなので戻しながら話を進めよう。

VC(Visual C++)では

IMAGE_TLS_DIRECTORYやPIMAGE_TLS_CALLBACKで検索すればサンプル的コードは沢山見つかるのだが、理論的な面で言うと、TLS ≪ Nynaeveが最も詳しいようだ。VC(Visual C++)環境を想定した解説ではあるが、内容はかなり熱い。上記の__declspec(thread)にはWindowsXPや複数モジュールでの懸念があるらしい事まで書いてある。これを全て日本語訳して解説するのはかなり骨が折れると同時に、本記事の分量を遥かに超えてしまいそうなので要点を抜き出していこう。

Nynaeveの記事のうち、特に重要なのはThread Local Storage, part 3: Compiler and linker support for implicit TLS ≪ Nynaeveだ。これによると、VC環境におけるTLSのサポートには2種類(2段階)ある。一つはWindowsのPEローダによるOS側の対応で、もう一つはCL.exe等のコンパイラ・リンカ側での対応。いずれにせよ、最終的にはWindowsのPEローダの機能に依存するが、まとめるために箇条書きにしてみる。

  • 最初のIMAGE_TLS_DIRECTORYの機能自体は、ローダのもの。
  • VCのラインタイム(CRT)ではIMAGE_TLS_DIRECTORYを_tls_usedという変数(tlssup.cで定義)に格納する。
  • リンカは_tls_usedという名前の変数を(ローダが扱うIMAGE_TLS_DIRECTORYとして).rdata$Tセクションに配置する。
  • _tls_usedはCRT側で確保されているのでユーザコードではexternで使用する。
  • IMAGE_TLS_DIRECTORYが指し示すコールバック関数のポインタ配列はNULL終端である。
  • コールバック関数のPIMAGE_TLS_CALLBACK型はDllMainの型と同様。
  • CRTが用意するTLS-Callback機能を使用するには".CRT$XL?"("?"は任意のアルファベット)という名前のセクションを使う。

つまり、PEローダの機能としてのIMAGE_TLS_DIRECTORYを使う方法が全ての根本だが、CRTの機能としてセクションへの配置によってTLS-Callbackを実現することができる。そしてコールバック関数のプロトタイプはDllMainと同様なので、fdwReasonでスレッド開始時・終了時の切り分けができる。

実際のコードとしては

#pragma section(".CRT$XLY",long,read)
extern "C" __declspec(allocate(".CRT$XLY"))
    PIMAGE_TLS_CALLBACK _xl_y = MyTlsCallback;

等と書くことでCRTの機能によるTLS-Callbackを利用出来るようだ。

なお、VC2005より前では".CRT$XL?"のセクション間にNULLを挿入してしまい、上手く動作しないらしい。

MinGWでは

で、あとはMinGWでどうなるのかが問題だ。正直、MinGWでサポートされる雰囲気なので、それを待つほうが賢明ではある。が、折角ここまで調べたのでコードも書いてみた。

その結果、以下のようなことがわかった。

  • TLSコールバック関数のアドレスは、VCの場合と同様.CRT$XLY等に置く必要がある。
  • IMAGE_TLS_DIRECTORY構造体は自分で変数として用意しなければならない。
  • IMAGE_TLS_DIRECTORYの変数名は_tls_usedとしなければならない。
  • _tls_used変数は.rdata$Tセクションに置く。
  • IMAGE_TLS_DIRECTORYを自分で用意する都合上、.tlsセクションに置く変数も用意する。
  • TLSコールバックはスレッドの開始・終了だけではなく、プロセスの開始・終了も捕捉する。
  • ただしプロセス開始時・終了時にはスレッド開始・終了は通知されない。

_tls_used変数をexternで使用するのではなく自分で定義するあたりが若干謎なのだが、このあたりはリンカやローダがweakシンボル等で上手くやるようになっているのかもしれない。

さて、これでやっと理屈の面では見当がついた。最後に実際のコード例を挙げてみよう。


この記事はmainとスレッドの前後処理(1)の続きです。次の記事はmainとスレッドの前後処理(3)です。

最終更新時間:2010年02月14日 04時58分03秒