据我所知,为避免输出混合,必须同步多个线程对cout和cerr的访问。在同时使用cout和cerr的程序中,单独锁定它们是否足够?或者同时写cout和cerr仍然不安全?
编辑说明:我知道cout和cerr在C ++ 11中是“线程安全的”。我的问题是,cout的写入和不同线程的cerr写入是否会以两次写入cout的方式相互干扰(导致交错输入等)。
答案 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不同,是对全局流对象(cout
,cin
,cerr
和clog
的插入和提取)线程安全。无需提供手动同步。但是,有可能由不同线程插入的字符在输出时会不可预测地交错;类似地,当多个线程从标准输入读取时,哪个线程将读取哪个令牌是不可预测的。
默认情况下,全局流对象的线程安全性处于活动状态,但可以通过调用流对象的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);
}
}
答案 3 :(得分:1)
这里已有几个答案。我将总结并解决它们之间的相互作用。
std::cout
和std::cerr
通常会汇集到一个文本流中,因此将它们锁定在一起会产生最有用的程序。
如果您忽略了该问题,默认情况下cout
和cerr
会将其stdio
对应方(它们是线程安全的as in POSIX别名,直至标准I / O函数(C ++14§27.4.1/ 4,比单独的C更强的保证)。如果您坚持使用这些函数,则会获得垃圾I / O,但不会出现未定义的行为(语言律师可能会将其与“线程安全”联系起来,而不管其是否有用)。
但是,请注意,虽然标准格式化I / O函数(例如读取和写入数字)是线程安全的,但操纵器可以更改格式(例如std::hex
为十六进制或std::setw
为限制输入字符串大小)不是。因此,人们通常不能认为省略锁是安全的。
如果您选择单独锁定它们,事情会更复杂。
对于性能,可以通过分别锁定cout
和cerr
来减少锁争用。它们是单独缓冲的(或无缓冲的),它们可以刷新到单独的文件中。
默认情况下,cerr
会在每次操作前刷新cout
,因为它们是“绑定的”。这会破坏分离和锁定,所以记得在做任何事情之前调用cerr.tie( nullptr )
。 (这同样适用于cin
,但不适用于clog
。)
stdio
标准规定cout
和cerr
上的操作不会引入比赛,但这并不完全是它的意思。流对象并不特殊;他们的基础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;