思うだけで学ばない日記 2.0

思うだけで学ばない日記から移転しました☆!よろしくお願いします。

std::iostreamのマルチスレッド対応はやっぱり糞くね?完璧に近い

一つ前のエントリでSafeStreamBufferの実装を明らかにするとともに、ストリームの状態を変更して元に戻す操作についてちょっと触れたわけだが
すわなち
<コード例1>

    // 基数とfillCharを変更
    ios::fmtflags sv_f = ost.setf(ios::dec, ios::basefield);
    ost.unsetf(ios::showpos);
    _Elem sv_c = ost.fill('0');

    // 日時出力
    ost << "["
        << setw(4) << year  << "-"
        << setw(2) << month << "-"
        << setw(2) << day   << "T"
        << setw(2) << hour  << ":"
        << setw(2) << min   << ":"
        << setw(2) << sec   << "."
        << setw(4) << msec  << "] ";

    // 元に戻す
    ost.fill(sv_c);
    ost.setf(sv_f);

このようにすれば良い、と思ったわけだが、よく考えたらは、一般に

  1. xの変更前の状態を保存
  2. xの状態を変更
  3. xを使って何かやる
  4. xの状態を(1.で保存した)元の状態に戻す

という操作は、アトミックにやらないと、マルチスレッド状況下で意図通りの操作にならない危険性がある。

しかし、上記コードのostがstd::coutのようなプログラムのどこからでも使える大域オブジェクトだったりすると*1、アトミック性の保証が困難な話になる。なぜなら、std::coutに対して出力する全てのコードが、同一の同期オブジェクトで排他制御するとは限らないからだ。
というわけで、SafeStreamBufferのコードはこの意味で潜在的なバグがある。サンプルコードに第三のスレッドを追加して、そのスレッドから、オリジナルのサンプルコードの同期オブジェクトsyncRootによる排他制御とは一切無関係にcout.setf(...)をしまくると時刻の表示を崩すことができてしまう(ハズ)。
これはしかし、std::ostreamがスレッドセーフを唄いつつ、std::ostreamの状態変更手段にstd::ostream::setf()かそれと同等な手段しかない*2というのが根本原因であって、SafeStreamBufferを提供する側からは如何ともし難いから堪忍してホスイ、、、*3
要はひとつのプログラムの中で上記のような第三のスレッドを作らねば良いのだ。もしくは、std::coutのような著名な大域オブジェクトに対してSafeStreamBufferを使うのでなく、ユーザープログラマーが完全に管理・掌握できるローカルなstd::ostreamオブジェクトに対してSafeStreamBufferする分には問題ない。SafeStreamBufferを便利に使うことができる。

【補足】

前回エントリのSafeStreamBufferでは時刻表示が全く暗黙のうちに行われていた。これをマニピュレータにするとカコイイ、と思われる向きもあるかもしれない。
例えばこんな風に書けるようにする。
<コード>

std::cout << timestamp << "Hello World!" << endl;

<実行結果>

[2012-02-26T23:39:00.1234] Hello World!

これは『C++マニアック』(URL省略)とか見れば実際全く簡単に実現できる。このようにしてもマルチスレッド状況下におけるSafeStreamBufferの上記問題はそのままだが、ただし状況が悪化するわけではない。いや悪化するなあorz、オリジナルの(一つ前のエントリの)SafeStreamBufferは時刻表示のためのstd::ostream::setf()はクリティカルセクション内でガードするが、上のtimestampのようにマニピュレータ実装された時刻表示におけるstd::ostream::setf()はガードされない。異なるスレッドから同時にtimestampマニピュレータが呼ばれた場合、(SafeStreamBufferは行の文字列内容をスレッド別に分けるものの)std::ostream::setf()の効果が意図通り変更/復旧されないかもしれない。*4いや悪化も何も問題は最初から存在しなかった!下記参照。

【補足2】

SafeStreamBuffer、自分で作っておきながらスゲー勘違いしてた;マルチスレッド状況下での問題とか存在しなかった!ヽ(°∀、°)ノ
確かにstd::ostreamにおいて、ストリームオブジェクトとフォーマットオブジェクトは1対1なのだけど、サンプルコード(下記)ではtest2()の中で、ストリームオブジェクト自体をスレッド別にost1、ost2として別個に作っているから、それぞれに対してどうsetf()しようが互いに影響しない。(本文で懸念したような、コード例1におけるostがstd::coutそのものであるという状況にはならない。)
というわけでSafeStreamBufferは全く安心して使えるハズ、
<サンプルコード(再掲)>

#include <process.h>

unsigned __stdcall ThreadFunc1(void* exinf)
{
    std::ostream& ost = *(std::ostream*)exinf;

    for (int n = 10; n > 0; --n) {
        for (int k = 100; k > 0; --k) {     // ostreamのバッファに収まらない行を作成
            ost << "Hello World! ";
        }
        ost << std::endl;   // 最後に改行兼明示的flush
    }
    return 0;
}

unsigned __stdcall ThreadFunc2(void* exinf)
{
    std::ostream& ost = *(std::ostream*)exinf;

    for (int n = 10; n > 0; --n) {
        for (int k = 100; k > 0; --k) {     // ostreamのバッファに収まらない行を作成
            ost << "This is a pen. ";
        }
        ost << std::endl;   // 最後に改行兼明示的flush
    }
    return 0;
}

void test2()
{
    CCritSec syncRoot;

    SafeStreamBuffer sfbuf1(std::cin, std::cout, syncRoot);
    std::ostream ost1(&sfbuf1);

    SafeStreamBuffer sfbuf2(std::cin, std::cout, syncRoot);
    std::ostream ost2(&sfbuf2);

    HANDLE threads[2];

    uintptr_t result1 = _beginthreadex(NULL, 0, ThreadFunc1, &ost1, CREATE_SUSPENDED, NULL); 
    threads[0] = (HANDLE)result1;

    uintptr_t result2 = _beginthreadex(NULL, 0, ThreadFunc2, &ost2, CREATE_SUSPENDED, NULL); 
    threads[1] = (HANDLE)result2;

    for (int i = 0; i < NUMELEMS(threads); ++i) {
        ResumeThread(threads[i]);
    }

    WaitForMultipleObjects(2, threads, TRUE, INFINITE);
}

*1:実際SafeStreamBufferの利用サンプルコードではそうなっている

*2:マニピュレータは結局出力先std::ostreamのメソッドを呼ぶので

*3:書式の変更を受け持つフォーマットオブジェクトがストリームオブジェクトと1対1対応というのではなしに、スレッド別にフォーマットオブジェクトを持てる設計ならばこんな問題は生じないはずなんだが、、というわけで今日のエントリの表題ではstd::ostreamの問題ということにした。撤回

*4:例えばもともとはios::hexだったのに、timestamp同士が競合した後、ios::decが効きっ放しになってしまう等。