Non-Blocking
私はBoost.IostreamsをCVSに登録される前から愛用しています。当初、Boost.IostreamsはブロッキングI/Oしかサポートしていませんでした。writeのインタフェースも
namespace boost { namespace io { template<typename T> void write( T& t, const typename char_type<T>::type* s, std::streamsize n ); template<typename T, typename Sink> void write( T& t, Sink& snk, const typename char_type<T>::type* s, std::streamsize n ); } } // End namespace boost::io
といった感じでした。ノンブロッキングI/OのサポートはCVS登録のしばらくあとです。
さて、ノンブロッキングI/Oのサポートの都合でかなりの修正が入りました。特にややこしいのはreadの戻り値です。
- 通常は読み込んだ文字の数
- 読み込む文字がまだ見当たらない場合は0
- EOFの場合は-1
POSIXのread()とは0/-1の意味合いがまるで逆です。これはこれでよくハマるのですが、もう一つノンブロッキングI/O特有の問題が潜んでいます。問題は0より大きな数を返した場合なのです。
ブロッキングI/OではEOFに到達しない限り、指定された文字数nきっかり読み出します。n未満の数を返す場合はEOFに到達したことを意味します。
一方、ノンブロッキングI/Oでは、EOFに達しなくてもn未満の数を返す場合があります。
このことからブロッキング/ノンブロッキング問わずにEOFを検知するためには
while (true) { std::streamsize amt = io::read(src, s, n); if (amt == n) break; // まだEOFじゃない else if (amt == -1) break; // EOF確定 else if (amt == 0) break; // WOULD_BLOCK確定 // n未満を読み出した // ブロッキングなら、次は絶対-1が返る // ノンブロッキングでも0か-1が返るはず(でも続けて読める場合もあり) s += amt; n -= amt; }
といった処理が必要になります。この読んでみないと終わりかどうか分からないというのがストリームの大きな特徴です。これとよく似た問題がソケットに対するpeekです。
// ヘッダサイズのデータがあったら読む(バグあり) bool read_header(int sock, header& h) { int amt = recv(sock, (char*)&h, sizeof(h), MSG_PEEK); if (amt == 0) throw eof_error(); // 適当な例外 if (amt == -1) throw error(amt); // 適当な例外 // ヘッダサイズに満たない場合は次回 if (amt != sizeof(h)) return false; recv(sock, &h, sizeof(h), 0); return true; }
一見、問題なさそうですが、このコードではヘッダ送信の途中でpeerがcloseした場合、ずっとfalseを返し続けます。たとえpeerがcloseしてもrecvが0や-1を返すことはないからです。amt == 0 が成り立つのはpeerが1バイトも送信せずにclose()した場合だけです。しかも、このソケット、当然ながらselectやpollをすると読み出し可能と判定されます。へたするとread_header()とselect()だけでCPU100%という事態になりかねません。実にややこしい。基本的にMSG_PEEKオプションは使ってはいけません。
というわけで今日のまとめ
ストリームは読んでみるまで終わりかどうか分からない