パイプがなぜか詰まってしまう問題 #43

公開日: 2013-01-19


ブラウザなどのGUIのアプリケーションではイベントドリブンという手法がよく使われる。イベントドリブンというのはイベントがやってきたら何か動作を起こすというもので、ボタンがクリックされたとか、キーボードが叩かれたとか、ネットワークからデータが届いたとか、そういったイベントに基づいてプログラムが動く。イベントがこない間はプログラムはメッセージループ(イベントループとも呼ばれる)というところで寝ている。

イベントがやってきたらプログラムはただちに目を覚まして、届いたイベントたちをメッセージループを回して処理しなければならない。目を覚まさなかったらプログラムは固まってしまう。メッセージループが正しく動くことはGUIのアプリケーションにとって非常に重要なことなのだ。

というわけで、メッセージループは重要なのだけど、最近遭遇したバグで、メッセージループが固まってしまう、というものがあった。この問題がごくまれに発生するということは以前から薄々気づいていたが再現させる手順もなく、滅多に起きないので問題の調査は後回しになっていた。

ある日、そんな問題があることもすっかり忘れてパフォーマンスを計るテストをしていると、UIが完全に固まるという問題に遭遇した。最初は何かタイミングの問題でも踏んでしまったのかと思ったが、もう一度同じ手順を繰り返すと確実に再現する。デバッガの上で実行しても同じように固まり、スタックトレースをみると、UIのイベントを処理するメインのスレッドのメッセージループ内で固まっている。これは例の問題じゃないか。

スタックトレースの一番上をみると write() というシステムコールで止まっていた。コードをみると Unix のパイプに1バイト書き込むところで止まっている。どうもパイプのバッファがいっぱいになってしまっているらしい。

しかしなぜ?さらにコードを読むと、パイプから1バイト read() するコードがあり、コメントには「パイプの中身は空か1バイトのどちらかである」といった旨のことが書かれている。もしそれが本当ならバッファがいっぱいになることもなかろうに。そもそもパイプに write() するのはなんのためなんだ?

この理由は割と簡単で、パイプに書き込むとイベントが発生するので、それを利用してメッセージループにタスク(仕事)を投げる、という仕組みのためだった。この仕組みを使うと、別のスレッドのメッセージループに仕事を投げたり、あるいは自分のスレッドに対して「後でやる」という仕事を投げたりできる。理屈は簡単だけどコードはかなりややこしい。

それでは、きっと大量のタスクがメッセージループに投げ込まれて、パイプが詰まってしまったのであろう。タスクを大量に投げるといえば、最近チェックインされた変更でそれらしいのを見たから原因はあれに違いない。そんなようなコメントを残して、残りの調査はタイムゾーンの違うところにいる同僚に頼んで帰ることにした。

帰宅してバグをチェックしてみると、さっそく「問題のコードを見てみたけどタスクがひとつ終わったらもうひとつ投げるというコードだから問題ないのでは?」といったコメントが返ってきていたいた。

しかし、問題のコードを特定の条件で動かして write のところで +1 、 read のところで -1 として数を数えてみると、またたくまに 60,000 を超えて、そのままメッセージループが固まってしまうらしい。やはり問題のコードが原因なのだ。そんなところまで確認して、自分は寝ることにした。

翌朝バグをチェックしてみると、大進展があった。write, read の挙動をよくみると、こんなことが起きていたのだ。

write write read, write write read, write write read, ...

つまり、write 2回に対して read が1回しか呼ばれていないのだ。同僚が調べたところによると、タスクの中から別のタスクを投げると write が2回発生してしまうとのこと。タスクの中から別のタスクを投げることはめずらしいことじゃないから、そんな簡単な問題があれば、もっと頻繁に固まってしまいそうなものだが、メッセージループはパイプに何かデータがある限りぐるぐる回ってタスクを処理しつつ read するという実装だったから、パイプにゴミが溜まっても放っておくとそのうち掃除されてしまうのであった。

というわけでこの問題は「タスクから別のタスクを投げる」という操作を約13万回 (Linux ではパイプのバッファサイズはデフォルト 64KiB =約 65,000 なので) 繰り返すとようやく発生するというレアな代物だったのだ。

この問題に対して、くだんの同僚は read で最大2バイト読むようにすればよい、というシンプルな解決策を提案していた。が、このあたりで氏のタイムゾーンでは夜になってしまい、コードの変更まで手が回らなかったようだ。

こうして、私が寝ている間に問題の解明が終わっていた。あと残っている作業といえば 2行くらいの変更とテストを書いて投げるだけだ。

さっそくそんなパッチを書いて投げると、コードレビューも無事に通って、ほどなくチェックインできた。コードの修正というおいしいところを持っていってしまったのは少し悪い気がするが。。

Satoru Takabayashi