前置き
本記事はFusic Advent Calendar 2018の23日目の記事です。
日ごろから、スレッドローカルな変数を定義することなどないので、あんまり身近でないですが、日ごろからお世話にはなっているので、そういった感じでスレッドローカルな初期値について考察します。
動機
きっかけはTwitter上で見かけたこのつぶやきでした:
\_\_thread in:file NOT "Invalid \_\_thread specifiers" extension:c でどうだろうと思ったら \_\_ が消えた(というのは、\_\_thread なんて誰も使ってない?)
— _ko1 (@_ko1) December 19, 2018
何かスレッドローカルに関して他のコードでも参考にしたいのかなー?といったふうに受け取ったので、仕事中のCI待ちに暇つぶしがてら調べてました。
その後、詳しくお聞きすると:
なんの話かというと、native thread のキャッシュを導入予定なんですが、ほんとに大丈夫なんだろうか、という。
— _ko1 (@_ko1) December 19, 2018
とのことで、何かスレッドごとの最適化でもするのかなーといった感じみたいです。
スレッドローカルストレージとは?
そもそもスレッドローカルストレージとはなんぞやですが、通常のグローバル変数がプロセスごとに割り当てる変数だとすると、スレッドローカルストレージの置かれるスレッドローカル変数はスレッドごとの領域に割り当てられています。そして、その領域をスレッドローカルストレージといいます。
広い意味で使うと、 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
の例を出したんですが:
あー。でもerrnoは毎回関数ごとに初期化しそう?
— _ko1 (@_ko1) December 19, 2018
というごもっともな指摘で玉砕です。
やはり、言語処理系でスレッドローカル変数をちゃんと使ってるものを探すべきだったのかもしれません。
そんな感じで、業務の隙間時間で調べてたことを記事にまとめさせてもらいました。
たまに気になったことがあればコードリーディングの訓練も兼ねて調べてみると楽しいかもしれません。