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オプションは使ってはいけません。
というわけで今日のまとめ
ストリームは読んでみるまで終わりかどうか分からない