我知道,为了避免输出混合,必须同步多个线程对 cout 和 cerr 的访问。在同时使用 cout 和 cerr 的程序中,单独锁定它们是否足够?还是同时写入 cout 和 cerr 仍然不安全?
编辑说明:我知道 cout 和 cerr 在 C++11 中是“线程安全的”。我的问题是,不同线程同时写入 cout 和写入 cerr 是否会以两次写入 cout 的方式相互干扰(导致交错输入等)。
我知道,为了避免输出混合,必须同步多个线程对 cout 和 cerr 的访问。在同时使用 cout 和 cerr 的程序中,单独锁定它们是否足够?还是同时写入 cout 和 cerr 仍然不安全?
编辑说明:我知道 cout 和 cerr 在 C++11 中是“线程安全的”。我的问题是,不同线程同时写入 cout 和写入 cerr 是否会以两次写入 cout 的方式相互干扰(导致交错输入等)。
如果你执行这个函数:
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";
}
cout
在 C++11 中,与 C++03 不同,全局流对象( 、cin
、cerr
和clog
)的插入和提取是线程安全的。无需提供手动同步。但是,由不同线程插入的字符可能会在输出时发生不可预测的交错;同样,当多个线程从标准输入读取时,无法预测哪个线程将读取哪个令牌。
sync_with_stdio
全局流对象的线程安全默认是激活的,但可以通过调用流对象的成员函数并false
作为参数传递来关闭它。在这种情况下,您将不得不手动处理同步。
同时写入 cout 和 cerr可能不安全!这取决于 cout 是否与cerr 相关联。见std::ios::tie。
“绑定流是一个输出流对象,在此流对象中的每个 i/o 操作之前刷新。”
这意味着,写入 cerr 的线程可能会无意中调用 cout.flush()。我花了一些时间弄清楚,这就是我的一个项目中 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);
}
}
这里已经有几个答案了。我将总结并解决它们之间的相互作用。
std::cout
并且std::cerr
通常会被汇集到单个文本流中,因此将它们锁定在最有用的程序中会产生共同的结果。
如果您忽略该问题,cout
并且cerr
默认情况下将它们的对应物别名为 POSIXstdio
中的线程安全,直到标准 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 )
如果锁是分开的。
这可能很有用;)
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);
}
我使用这样的东西:
// 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;