iostream线程安全,必须cout和cerr分开锁定?

时间:2013-02-01 00:16:00

标签: c++ thread-safety cout

据我所知,为避免输出混合,必须同步多个线程对cout和cerr的访问。在同时使用cout和cerr的程序中,单独锁定它们是否足够?或者同时写cout和cerr仍然不安全?

编辑说明:我知道cout和cerr在C ++ 11中是“线程安全的”。我的问题是,cout的写入和不同线程的cerr写入是否会以两次写入cout的方式相互干扰(导致交错输入等)。

6 个答案:

答案 0 :(得分:12)

如果执行此功能:

void f() {
    std::cout << "Hello, " << "world!\n";
}

从多个线程中,您将获得两个字符串"Hello, ""world\n"的或多或少的随机交错。那是因为有两个函数调用,就像你编写了这样的代码:

void f() {
    std::cout << "Hello, ";
    std::cout << "world!\n";
}

要防止交错,您必须添加一个锁:

std::mutex mtx;
void f() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Hello, " << "world!\n";
}

也就是说,交错问题与cout 无关。它是关于使用它的代码:有两个单独的函数调用插入文本,所以除非你阻止多个线程同时执行相同的代码,否则在函数调用之间有一个线程切换的可能性,这就是给你的交错。

请注意,互斥锁不会阻止线程切换。在上面的代码段中,它可以防止同时从两个线程执行 f()的内容;其中一个线程必须等到另一个完成。

如果您写入cerr,则会遇到相同的问题,除非您确保从未有两个线程进行这些插入器函数调用,否则您将获得交错输出同时,这意味着两个函数必须使用相同的互斥锁:

std::mutex mtx;
void f() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Hello, " << "world!\n";
}

void g() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cerr << "Hello, " << "world!\n";
}

答案 1 :(得分:7)

在C ++ 11中,与C ++ 03不同,是对全局流对象(coutcincerrclog的插入和提取)线程安全。无需提供手动同步。但是,有可能由不同线程插入的字符在输出时会不可预测地交错;类似地,当多个线程从标准输入读取时,哪个线程将读取哪个令牌是不可预测的。

默认情况下,全局流对象的线程安全性处于活动状态,但可以通过调用流对象的sync_with_stdio成员函数并将false作为参数传递来关闭它。在这种情况下,您必须手动处理同步。

答案 2 :(得分:5)

可能同时写入cout和cerr是不安全的! 这取决于是否将cout 绑定与cerr相关。请参阅std::ios::tie

  

“绑定的流是以前刷新的输出流对象   此流对象中的每个i / o操作。“

这意味着,cout.flush()可能被写入cerr的线程无意中调用。 我花了一些时间才弄明白,这就是我的一个项目中cout输出中随机遗漏行结尾的原因:(

使用C ++ 98 cout不应该与cerr绑定。但是,尽管使用MSVC 2008(我的经验),它仍然是标准。使用以下代码时一切正常。

std::ostream *cerr_tied_to = cerr.tie();
if (cerr_tied_to) {
    if (cerr_tied_to == &cout) {
        cerr << "DBG: cerr is tied to cout ! -- untying ..." << endl;
        cerr.tie(0);
    }
}

另请参阅:why cerr flushes the buffer of cout

答案 3 :(得分:1)

这里已有几个答案。我将总结并解决它们之间的相互作用。

典型地,

std::coutstd::cerr通常会汇集到一个文本流中,因此将它们锁定在一起会产生最有用的程序。

如果您忽略了该问题,默认情况下coutcerr会将其stdio对应方(它们是线程安全的as in POSIX别名,直至标准I / O函数(C ++14§27.4.1/ 4,比单独的C更强的保证)。如果您坚持使用这些函数,则会获得垃圾I / O,但不会出现未定义的行为(语言律师可能会将其与“线程安全”联系起来,而不管其是否有用)。

但是,请注意,虽然标准格式化I / O函数(例如读取和写入数字)是线程安全的,但操纵器可以更改格式(例如std::hex为十六进制或std::setw为限制输入字符串大小)不是。因此,人们通常不能认为省略锁是安全的。

如果您选择单独锁定它们,事情会更复杂。

单独锁定

对于性能,可以通过分别锁定coutcerr来减少锁争用。它们是单独缓冲的(或无缓冲的),它们可以刷新到单独的文件中。

默认情况下,cerr会在每次操作前刷新cout,因为它们是“绑定的”。这会破坏分离和锁定,所以记得在做任何事情之前调用cerr.tie( nullptr )。 (这同样适用于cin,但不适用于clog。)

stdio

分离

标准规定coutcerr上的操作不会引入比赛,但这并不完全是它的意思。流对象并不特殊;他们的基础streambuf缓冲区是。

此外,调用std::ios_base::sync_with_stdio旨在删除标准流的特殊方面 - 允许它们像其他流一样进行缓冲。尽管该标准未提及sync_with_stdio对数据竞争的任何影响,但快速查看libstdc ++和libc ++(GCC和Clang)std::basic_streambuf类表明它们不使用原子变量,因此它们可能用于缓冲时创建竞争条件。 (另一方面,libc ++ sync_with_stdio实际上什么都不做,所以如果你调用它并不重要。)

如果你想要额外的性能而不管锁定,sync_with_stdio(false)是个好主意。但是,执行此操作后,如果锁是分开的,则需要锁定cerr.tie( nullptr )

答案 4 :(得分:0)

这可能很有用;)

inline static void log(std::string const &format, ...) {
    static std::mutex locker;

    std::lock_guard<std::mutex>(locker);

    va_list list;
    va_start(list, format);
    vfprintf(stderr, format.c_str(), list);
    va_end(list);
}

答案 5 :(得分:0)

我使用这样的东西:

// Wrap a mutex around cerr so multiple threads don't overlap output
// USAGE:
//     LockedLog() << a << b << c;
// 
class LockedLog {
public:
    LockedLog() { m_mutex.lock(); }
    ~LockedLog() { *m_ostr << std::endl; m_mutex.unlock(); }

    template <class T>
    LockedLog &operator << (const T &msg)
    {
        *m_ostr << msg;
        return *this;
    }

private:
    static std::ostream *m_ostr;
    static std::mutex m_mutex;
};

std::mutex LockedLog::m_mutex;
std::ostream* LockedLog::m_ostr = &std::cerr;