LoginSignup
8
2

More than 5 years have passed since last update.

Thread Local Storageの初期値について調べた

Posted at

前置き

本記事はFusic Advent Calendar 2018の23日目の記事です。
日ごろから、スレッドローカルな変数を定義することなどないので、あんまり身近でないですが、日ごろからお世話にはなっているので、そういった感じでスレッドローカルな初期値について考察します。

動機

きっかけはTwitter上で見かけたこのつぶやきでした:

何かスレッドローカルに関して他のコードでも参考にしたいのかなー?といったふうに受け取ったので、仕事中のCI待ちに暇つぶしがてら調べてました。

その後、詳しくお聞きすると:

とのことで、何かスレッドごとの最適化でもするのかなーといった感じみたいです。

スレッドローカルストレージとは?

そもそもスレッドローカルストレージとはなんぞやですが、通常のグローバル変数がプロセスごとに割り当てる変数だとすると、スレッドローカルストレージの置かれるスレッドローカル変数はスレッドごとの領域に割り当てられています。そして、その領域をスレッドローカルストレージといいます。
広い意味で使うと、 pthread_t から参照できる情報などもスレッドローカルストレージになるかと思われます。

スレッドごとにデータがあって嬉しいものの不動の代表といいますと、UNIXのAPIで使う errno です。
errno 自体がマルチスレッドという概念が導入される前のものなのでスレッドローカルにしないとよそのスレッドでのシステムコール呼び出しの結果で errno が上書きされるという事故に繋がります。マルチスレッドモードのフラグを立てたりすると自動的にスレッドローカルに切り替わるといったlibcの実装もあるようです。

他に、スレッドごとに持ちたいデータの参照元として使うなどマルチスレッドなプログラムを書く場合には不可欠な存在です。

C言語で書いてるプログラムでとある変数とスレッドローカルにしたい場合は、GCC拡張の __thread を使ったり、C11から導入された _Thread_local<thread.h> をインクルードすると使える thread_local ストレージクラス指定子を使います。詳しくはcppreference.comのページを参照してください。規格のどこに定義があるのかととか詳しいのでおすすめです。

また、プログラムで必ずしも必要でない場合は、pthread_key_tを使ってスレッドローカルストレージを動的に割り付けるといったことも可能です。C11準拠でない処理系の場合にもこちらの方法を使うようです。

元ツイートの方が知りたかったこと

どうやら、明示的に初期化をしなかった場合の挙動を知りたかったようです。

規格上は、 static ストレージ期間に近い thread ストレージ期間なので、 static がプロセスの初期化と破棄時にデフォルトの初期値(数値だったら0、ポインタだったらNULLなど)で割り当てられるのが、代わりにスレッドの初期化と破棄時になります。
ということは、グローバル変数で明示的に初期化をしなかった場合の挙動をスレッドという単位に落とし込んで考えればよさそうです。

ただ、それはあくまで規格上で、実装というのは往々にして規格を完全に再現することを期待してはいけないので、もう少し実装よりなところを調べました。

ELFでは

ELF Handling For Thread-Local Storageというのがあったので読んでみました。
けっこういろいろ書いてあるんですが、わたしが知りたかったのは、どうゆうふうにデータが扱われるかだったので、 .tbss.tdata というセクションがあるとわかれば十分でした。

どうやら、 .bss.data のスレッド版である .tbss.tdata というセクションがあるのでやってることは .bss.data と変わらないとあたりがついたので、明示的な初期値がない場合は .bss セクションみたいにゼロで埋められるのではないかと思われます。
(本当は、プログラムの動的ローダーまで読みたかったんですが、そこら辺はまた今度にしました。。。)

Clangの生成するコードのテスト

このファイルを見ると

int __thread x;

というコードが

@x = thread_local global i32 0

というスレッドローカルなグローバルでint32_tで0初期化な領域になるように期待しているようです。
これが、どうネイティブコードに変換されるかまでは特におってないですが、ひとまず規格で期待しているようなコードにLLVM IRの段階ではないっていることはわかりました。

glibcでは

やっぱり実際に使われているコードというのを参照するのが元ツイートの人が求めていたことだと思うので、他に調べていたらosdev.orgの解説ページで、 errno が最初に例として出ていたので、代表的な移植性の高いlibcであるglibcとmusl libcを調べてみることにしました。
musl libcでは、 pthread_t のデータに errno が埋めてあったので今回は不適当だったのでglibcの例を出します。

csu/errno.cというファイルに

__thread int errno;

と求めてやまなかった書き方のスレッドローカル変数の定義がありました!

おまけ: Qt

Qtのコードベースはとても大きいのでなんか叩けば出てくるだろうと思って探したら、コンパイラのテストおぼしきものが出てきました。
QGlobal関連のファイルで以下のようなコードがありました。

#ifdef Q_COMPILER_THREAD_LOCAL
static thread_local int gt_var;
void thread_local_test()
{
    static thread_local int t_var;
    t_var = gt_var;
}
#endif

ただ、他で thread_local_test が参照されるといったことがなかったのでコンパイラのテスト用だけみたいです。
それから、QtはC++で書かれているのでCと初期化の事情がそもそも違うので例としてあんまりよろしくなかったりします。

おまけ: Rustのスレッドローカル変数定義

pthread_key_t を使った方に近い感じみたいです。
std::thread_local! マクロがあるんですが、内部的に pthread_key_t に近いものに落としてそうなAPIでした。
もしかしたらLLVM intrinsicでがんばって最適化してるのかもしれませんが。。。

おまけ: C11/C++11 TLS変数への間接アクセス

元記事を読めばわかるんですが、C++11になるとC++の生存期間の定義があるのでより厳密になるみたいです。
CとC++はやっぱり別言語だなとこうゆうときに感じますね。

結論とか

スレッドローカルであろうとつべこべ言わず、変数はちゃんと初期化しましょう。(え

というのも、「初期化してない変数と.bssセクション」という神記事があるのでそれを読んでください。(というか、最初からこの神記事で十分だった気がします。。。)
.bss のちゃんとした解説もありますし、ABIによっては敢えてゼロ初期化しないこともあるというのは(現代でもあるのか知りませんが)大事です。

ちなみに、glibcの errno の例を出したんですが:

というごもっともな指摘で玉砕です。
やはり、言語処理系でスレッドローカル変数をちゃんと使ってるものを探すべきだったのかもしれません。

そんな感じで、業務の隙間時間で調べてたことを記事にまとめさせてもらいました。
たまに気になったことがあればコードリーディングの訓練も兼ねて調べてみると楽しいかもしれません。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2