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

Posted

技术标签:

【中文标题】iostream 线程安全,必须分别锁定 cout 和 cerr 吗?【英文标题】:iostream thread safety, must cout and cerr be locked separately? 【发布时间】:2013-01-16 06:02:22 【问题描述】:

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

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

【问题讨论】:

它永远不会“不安全”。你可能只是没有得到你所期望的。 我想澄清一下。使用一个全局锁写入 cout 和 cerr 与分别使用单独的锁在行为上是否存在差异? 您可以使用不同的锁。它们不相互依赖。 或许将这个问题表述为“可能 cout 和 cerr 被分别锁定?” 另见Is cout synchronized/thread-safe? 【参考方案1】:

如果你执行这个函数:

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无关。这是关于使用它的代码:有两个单独的函数调用插入文本,所以除非你阻止多个线程同时执行相同的代码,否则函数调用之间可能会发生线程切换,这就是给你的交错。

请注意,互斥锁不会阻止线程切换。在前面的代码sn-p中,它防止两个线程同时执行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";

【讨论】:

应该注意的是,如果操作系统将coutcerr 指向同一个源(比如...控制台),那么文本将被交错,除非它们共享相同互斥体。 操作系统和库与它有很大关系。 Linux 中的线程可以随意输出到标准输出,无需锁定,但单独的文本行不会被另一个线程的标准输出打乱。到 stderr 的输出没有以同样的方式缓冲,所以它会被弄乱。这种行为差异会导致某些版本的 Eclipse CDT 出现问题;编译器错误出现在 stderr 上,并且没有出现在 Eclipse 控制台中编译器的 stdout 的正确位置。【参考方案2】:

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

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

【讨论】:

【参考方案3】:

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

【讨论】:

有趣。可以肯定的是,这并不像 UB 那样不安全,它“只是”表现不佳。 哇...这让我发现了一些难以发现的错误。【参考方案4】:

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

通常,

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 )

【讨论】:

如果你确实想要单独的锁定,我有 created 一个库。【参考方案5】:

这可能有用;)

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);

【讨论】:

【参考方案6】:

我使用这样的东西:

// 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;

【讨论】:

以上是关于iostream 线程安全,必须分别锁定 cout 和 cerr 吗?的主要内容,如果未能解决你的问题,请参考以下文章

std::cout 插入运算符的线程安全

printf的线程安全性以及cout的线程不安全性验证,以及意外收获

阅读时是不是需要锁定非线程安全的集合?

POSIX 部分写入、线程安全和锁定

线程安全std :: map:锁定整个地图和各个值[重复]

自动验证一些线程限制? (C#)