11

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

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

4

6 回答 6

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";
}
于 2013-02-01T15:23:43.667 回答
9

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

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

于 2013-02-01T00:27:19.050 回答
5

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

另见:为什么 cerr 刷新 cout 的缓冲区

于 2014-02-09T23:23:50.997 回答
1

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

通常,

std::cout并且std::cerr通常会被汇集到单个文本流中,因此将它们锁定在最有用的程序中会产生共同的结果。

如果您忽略该问题,cout并且cerr默认情况下将它们的对应物别名为 POSIXstdio中的线程安全,直到标准 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

该标准说,操作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 )如果锁是分开的。

于 2016-03-15T05:48:37.927 回答
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);
}
于 2018-06-02T22:40:35.423 回答
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;
于 2019-02-06T17:01:47.327 回答