デバッガとスレッドとイベント

実行パスが一つしか無くて、ユーザやらネットワークやら、外部とのインタラクションもブロッキングして読んで問題ないようなプログラムならいいんですが、まぁなんかそうもいかないことも多く、そういう時はスレッドやら select/epoll やら使ってごにょごにょしてるわけです。でまぁ、あろはさんのとこのコメント欄を荒らさせてもらったんですが、まぁデバッガに欲しいスレッドやらイベントサポートの話。なんとなくもやもや思ってることを適当にまとめてみます。

まずまだマシなスレッドの方。まぁ適当に 100 スレッドくらいのスレッドプールの中の一つのスレッドが SEGV したとする。さあデバッガの出番。とりあえず適当に現在のスレッドの値を調べてみたところ、なんかおかしな値が入ってたとします。でまぁ、シングルスレッドでバグが無いとしたら、レースコンディションです。でそいう時にとりあえず、

thread apply all bt

とかをぐぐって思い出すのに5分くらいかかる…というのはともかくとして、まぁこれを実行してやると、 101 個のバックトレース(100スレッドプール+メインスレッド)が出るんです。ちょっときついものがあります。ここで欲しいなぁと思うのは、

  • 誰が作ったスレッドなのかという作り手情報。 thread 作った時に backtrace 取っておくか、プログラムからスレッドの名前をつけられると良いと思う。
  • スレッドの分類。メインスレッドとスレッドプール内のゴミスレッドが同列にドバーと表示されても困るというような。スレッドに名前がつけられれば実現できそう。
  • スレッドプールの概念を理解した上で、誰がスレッドプールに現在のタスクを渡したか、という情報。 thread pool にタスク突っ込まれた時に backtrace 取って置くか、プログラムからタスクの名前をつけられると良いと思う。
  • どのスレッドが待ち状態でないのか。 100 スレッドもあればスレッドプール内のスレッドのほとんどが遊んでるような状態だったりすることもあると思う。時間のかかる system call に入ってない子だけのスタックトレースが出せれば、助けになるケースがあると思う。例えばレースをひきおこすデータ操作を行なったスレッドが次の wait に入るまでに SEGV が起きた時など。

まとめるとスレッドはアイデンティティ持ってなさすぎ、というのが問題なんじゃないかなぁと漠然と思う。

イベントっていうか select/epoll の方は、ちょっと前に書いた例をまた持ってくるとする。今書いてるサーブレットだかなんだか知らないサーバの、メインの処理がこんなものだとする。

 void handleRequest(int fd) {
   read(fd);
   foo();
   requestAnotherServer();
   bar();
   write(fd);
 }

リクエストが来たら、それを読んで他のサーバーに問いあわせをして、その結果を使ってレスポンスを返す、と。でまぁスレッドの方は、

 threadLibrary.create(&handleRequest);

とかで登録して、 requestAnotherServer の間ぼんやり待ってても十分な程度のスレッドがスレッドプールにあるとすると、まぁこれでだいたい実装完了、となると思う。 read(), foo(), requestAnotherServer(), bar(), write() のどこで落ちたとしても、 handleRequest を呼び出した子のスタックトレースなんかも取れるし、まぁ問題ない、と。

select/epoll の方は、

 eventLibrary.register(&handleRequest);

とかで登録したとして、まさか 1 秒間実行を止めるわけにはいかないから、

 void handleRequestDone(int fd) {
   bar();
   write(fd);
 }
 
 void handleRequest(int fd) {
   read(fd);
   foo();
   requestAnotherServerAsync(bind(handleRequestDone, fd));
 }

などと、処理をわけることになる、と。 requestAnotherServerAsync っていうのは他のサーバへのリクエスト処理が終わったら handleRequestDone を呼ぶような関数とする。こういう時に、 bar() で落ちた場合、スタックトレースが、

 bar
 handleRequestDone
 EventLibrary::eventLoop
 ...
 main

とかになって、 handleRequestDone を呼んだ時のコンテキストが失われてしまってるのが、とてもめんどくさい、と。これはスレッドプールにタスクを置いたのは誰か、と似たような話かなぁ。結局イベントループにタスクを置いたのは誰かがわかれば良いので。ただまぁイベントループの場合は、タスクを置く的な行為がスレッド以上に必要不可欠なので、困るケースが多いと思う。(おまけ: タスク置く、っていうのは、本質的にはスレッドのケースにもいずれ必要な処理で、スレッドプールのサイズ広げてゴマかしてるだけかもだけど(まぁソケットはれる限界数と同じオーダのスレッド作れればサーバの場合は問題ないよね))

でデバッガに必要なのは何か、っていうと、今上げたようなものは C++ とかの場合は言語ではなくてライブラリなので、コンパイラデバッグ情報を通じてやりとりしているように、ライブラリと情報をやりとりできる必要があるんじゃないかなぁと思う。まずは現状ライブラリはスレッドの名前やらスレッドの作られたコンテキストやらタスクの置かれたコンテキストなどを全く保存してないので、とにもかくにもそいう情報を保存してもらう必要があるんじゃないかなぁと思う。

あるいは、デバッガが上記の関数コールを適当に奪ってコンテキストを保存する、とかもアリだと思うけど、それだとプロセスアタッチした時とかは十分なデバッグができないことになると思う。

どんな感じでやればコンパイラ/ライブラリ/デバッガが仲良くできるかなぁ…と。タスクを置く時のコンテキスト…みたいな話だと、

template <hoge...>
struct binder {
  operator(...) {
    fp(...);   // !!!!!
  }

  binder(...) : ctx(getStackTrace()), fp(...) {...}

  const Stacktrace ctx;
  void (T::*fp)(...)  __attribute__((have_context, ctx));
}

とかいう感じで、 ctx にスタックトレースを保存して、関数ポインタ fp を呼ぶ時にコンテキストが参考としてついてるよー的な情報を __attribute__ とかでつけておいて、したらまぁその attribute がついてる関数 fp() を呼ぶところでコンパイラデバッグ情報になにやらヒントを書き出して、それらをデバッガが読む…とかすりゃまぁいい、と思う。でまぁこんなことやるためには、 getStackTrace とかが標準的に用意されてる必要があるし、ライブラリは __attribute__ でアノテートしにゃならんし、コンパイラは __attribute__ をサポートしにゃならんし、デバッグ情報フォーマット (DWARF とか) は fp の呼び出し元と ctx の関係 (たぶん fp-1 のところにコンテキスト情報がありまっせー的な感じ) を保持するフォーマットを作らなきゃならんし…という感じで、どう考えてもデバッガだけの力じゃ無理で、環境全体が頑張らなきゃいかん気がするんだよな。

デバッガだけで無理に頑張る例としては STL の内容物が読めるようになる gdb script みたいなのはあちこちにあると思うんだけど、ライブラリの private メンバの名前とかが変わるだけで使えなくなっちゃうんだよね…こういうヤツ:

http://www.stanford.edu/~afn/gdb_stl_utils/

まぁこのくらいは頑張ればなんとでもなるのだけど、やっぱコンパイラ/ライブラリからのサポート欲しいかなぁ…とか思うところなのです。

既にできるよーみたいなのがあれば教えていただけるとすごい嬉しいです。プロプラなものだと実現してるものとか結構ありそうですし、どうやってやってるかとか気になる。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h