IOstream线程安全,必须将cout和cerr分开锁定



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

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

如果执行此函数:

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

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

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";
}

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

默认情况下,全局流对象的

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

同时写给 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);
    }
}

另请参阅:为什么 cerr 会刷新 cout 的缓冲区

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

通常

std::coutstd::cerr通常会汇集到单个文本流中,因此将它们锁定在公共会导致最有用的程序。

如果忽略此问题,则默认情况下,coutcerr为其stdio对应项(如 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 )

这可能

很有用;)

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;

最新更新