如何轻松使 std::cout 线程安全?

Posted

技术标签:

【中文标题】如何轻松使 std::cout 线程安全?【英文标题】:How to easily make std::cout thread-safe? 【发布时间】:2013-01-21 00:07:20 【问题描述】:

我有一个多线程应用程序,它大量使用std::cout 进行日志记录而没有任何锁定。在这种情况下,如何轻松添加锁机制使std::cout线程安全?

我不想搜索std::cout 的每次出现并添加一行锁定代码。太乏味了。

有更好的做法吗?

【问题讨论】:

这实际上是讨论的主题之一,并在this video 中给出了示例。它的使用时间大约是 48 分钟,而实现是在此之前的一段时间,因为它之前有一个字符串示例。 std::cout 已经是线程安全的。你的意思是你希望每次刷新都序列化实际的文本块? 第 27.4.1 节:Concurrent access to a synchronized (27.5.3.4) standard iostream object’s formatted and unformatted in- put (27.7.2.1) and output (27.7.3.1) functions or a standard C stream by multiple threads shall not result in a data race (1.10). 然而,标准说:“[注意:如果用户希望避免交错字符,他们仍必须同步多个线程对这些对象和流的并发使用。-结束注释]” @xmllmx:这又回到了我所说的:它已经是线程安全的,你想进一步序列化每次刷新的字符显示吗? 【参考方案1】:

虽然我不能确定这适用于 std 库的每个编译器/版本 但在我使用的代码库中,std::cout::operator<<() 它已经是线程安全的了。

我假设您真正想要做的是阻止 std::cout 在跨多个线程与每个字符串多次连接 operator<< 时混合字符串。

字符串出现乱码的原因是operator<< 上存在“外部”竞争 这可能会导致这样的事情发生。

//Thread 1
std::cout << "the quick brown fox " << "jumped over the lazy dog " << std::endl;

//Thread 2
std::cout << "my mother washes" << " seashells by the sea shore" << std::endl;

//Could just as easily print like this or any other crazy order.
my mother washes the quick brown fox seashells by the sea shore \n
jumped over the lazy dog \n

如果是这种情况,有一个比创建自己的线程安全 cout 或实现与 cout 一起使用的锁要简单得多的答案。

在将字符串传递给 cout 之前只需编写字符串

例如。

//There are other ways, but stringstream uses << just like cout.. 
std::stringstream msg;
msg << "Error:" << Err_num << ", " << ErrorString( Err_num ) << "\n"; 
std::cout << msg.str();

这样你的stings就不会出现乱码,因为它们已经完全形成了,另外,在调度它们之前完全形成你的字符串也是一个更好的做法。

【讨论】:

我认为重要的是要注意您指的是特定的实现。哪一个,它在哪里保证线程安全?我还没有看过自己,但我怀疑任何最近的 C++ 标准都保证 operator&lt;&lt;(std::ostream&amp;, const std::string&amp;) 是原子的。如果没有保证,那么在某些实现中,实现可能仍会以块的形式将字符串写入,将来自不同线程的数据交错。 保证 std::cout (一般不是 std::ofstream)。 比这更糟糕。我正在使用 Xcode(在 OS X 上)并看到逐个字符的交错,使消息几乎不可读。一个受互斥保护的 cout 包装器可能是最好的方法。【参考方案2】:

注意:这个答案是 C++20 之前的,所以它不使用带有单独缓冲的 std::osyncstream,而是使用锁。

我猜你可以实现自己的类,它包装cout 并将互斥锁与它相关联。那个新类的operator &lt;&lt; 会做三件事:

    为互斥锁创建一个锁,可能会阻塞其他线程 进行输出,即对包装的流和传递的参数执行操作符&lt;&lt; 构造一个不同类的实例,将锁传递给该类

这个不同的类会将锁和委托操作符&lt;&lt; 保留给包装的流。第二个类的析构函数最终会销毁锁并释放互斥体。

因此,您作为单个语句编写的任何输出,即作为&lt;&lt; 调用的单个序列,只要您的所有输出都通过具有相同互斥锁的该对象,就会自动打印。

让我们称这两个类为synchronized_ostreamlocked_ostream。如果sync_coutsynchronized_ostream 的一个实例,它包装了std::cout,那么序列

sync_cout << "Hello, " << name << "!" << std::endl;

将导致以下操作:

    synchronized_ostream::operator&lt;&lt; 将获得锁 synchronized_ostream::operator&lt;&lt; 将“你好”的打印委托给cout operator&lt;&lt;(std::ostream&amp;, const char*) 会打印“你好,” synchronized_ostream::operator&lt;&lt; 将构造一个 locked_ostream 并将锁传递给它 locked_ostream::operator&lt;&lt; 会将name 的打印委托给cout operator&lt;&lt;(std::ostream&amp;, std::string) 会打印名字 感叹号和结束线操纵符对cout 进行相同的委托 locked_ostream 临时被破坏,锁被释放

【讨论】:

这可以简化一点。首先,如果任何代码不符合规则,它仍然会与cout 混淆,这意味着您必须更改任何出现的cout。在这种情况下,您也可以将其替换为(cout_lock(), cout),其中cout_lock() 是专门用于同步访问cout 的作用域锁类型。这将创建一个临时对象,直到下一行,而表达式 (A, B) 的结果是 B 由于逗号运算符。我声称这是更少的代码,如果你愿意,你仍然可以将互斥锁锁定超过一行。 @doomster:使用我的设置也可以为多个表达式明确维护locked_ostream,但您的方法也很有趣。不过,它似乎确实需要一个宏,这是一种相当非 C++ 的处理问题的方式。否则,总是对锁进行编码将变得乏味。 @MvG:他没有使用宏,他使用的是逗号运算符。无论如何,这可以简化。将cout 替换为一个对象,该对象在收到'\n' 之前进行缓冲,然后锁定,将整个缓冲区转储到cout,然后解锁,一次完成。 @MvG 如你所知,C++20 带来了osyncstream,它类似于你的synchronized_ostream 包装器。您介意更新您的答案以反映这一点吗? @LF: std::osyncstream 包含在 C++20 中,但仍不可用。您可以通过检查编译器对synchronized buffered ostream here 的支持来验证这一点。在它可用之前(或者您使用非官方实现),您可能必须使用锁。【参考方案3】:

我真的很喜欢 Nicolás 在 this question 中给出的创建临时对象并将保护代码放在析构函数上的技巧。

/** Thread safe cout class
  * Exemple of use:
  *    PrintThread << "Hello world!" << std::endl;
  */
class PrintThread: public std::ostringstream

public:
    PrintThread() = default;

    ~PrintThread()
    
        std::lock_guard<std::mutex> guard(_mutexPrint);
        std::cout << this->str();
    

private:
    static std::mutex _mutexPrint;
;

std::mutex PrintThread::_mutexPrint;

然后,您可以在任何线程中将其用作常规 std::cout

PrintThread << "my_val=" << val << std::endl;

该对象以常规ostringstream 的形式收集数据。一旦达到昏迷状态,对象就会被销毁并刷新所有收集到的信息。

【讨论】:

我想知道你是否可以在标题中使用class PrintThreadType ; PrintThreadType&amp; getPrintThread() static PrintThreadType a; return a; PrintThreadType&amp; PrintThread = getPrintThread(); 来回避 odr?可能没用。 你必须小心这个想法。不应允许 C++ 中的析构函数抛出异常。默认情况下,在 C++11 中(并且,我认为,超越),如果异常从析构函数中传播出来,它将调用 std::terminate。 最后一行是做什么的? std::mutex PrintThread::_mutexPrint; @Ari, _mutexPrint 是一个静态变量(PrintThread 的所有实例通用)(参见:en.cppreference.com/w/cpp/language/static), 只是初始化一个对象(C++11 起)(参见:en.cppreference.com/w/cpp/language/value_initialization)。所以最后一行初始化静态变量一次。 @KefSchecter,如果这在实践中确实是一个问题,我想可以尝试将析构函数代码包装在一些try catch(...) 中以使错误静音而不是终止。但这确实是这种方法的局限性。【参考方案4】:

由于C++20,你可以使用std::osyncstream包装器:

http://en.cppreference.com/w/cpp/io/basic_osyncstream


  std::osyncstream bout(std::cout); // synchronized wrapper for std::cout
  bout << "Hello, ";
  bout << "World!";
  bout << std::endl; // flush is noted, but not yet performed
  bout << "and more!\n";
 // characters are transferred and std::cout is flushed

它提供了保证所有输出到相同的最终结果 目标缓冲区(上面示例中的 std::cout )将没有 数据竞争,并且不会以任何方式交错或乱码,只要 因为对最终目标缓冲区的每次写入都是通过 (可能不同)std::basic_osyncstream 的实例。

或者,您可以使用临时:

std::osyncstream(std::cout) << "Hello, " << "World!" << '\n';

【讨论】:

我不认为这些选项可供我使用,我正在使用基于 Qt 5.15.1 Clang 11.0 Apple 64 位的 Qt Creator 版本 4.13.2。我没有看到标题。 我需要为所有cpp文件声明一次std::osyncstream bout(std::cout)还是可以在每个cpp中再次声明? 据我了解(我不是很自信),理想情况下,每个线程应该有一个 osyncstream,并且它们不应该在线程之间共享。或者,您可以使用临时的。我将使用文档中的示例更新我的答案。 我遇到致命错误:syncstream:没有这样的文件或目录,有没有办法获取头文件? @csisy 不。这只是意味着从多个线程使用std::cout 不会是UB。您仍然可以获得“错误”的输出。例如。如果线程 A 执行 std::cout &lt;&lt; "A: " &lt;&lt; 0 &lt;&lt; " "; 而线程 B 执行 std::cout &lt;&lt; "B: " &lt;&lt; 1 &lt;&lt; " ";,您可以获得类似 A: B: 0 1 的输出。那是“安全的”,但也可能不是您想要的。但是用std::osyncstream(std::cout) 替换std::cout 可以保证你得到A: 0 B: 1B: 1 A: 0【参考方案5】:

为了快速调试 c++11 应用程序并避免交错输出,我只编写了如下的小函数:

...
#include <mutex>
...
mutex m_screen;
...
void msg(char const * const message);
...
void msg(char const * const message)

  m_screen.lock();
  cout << message << endl;
  m_screen.unlock();

我将这些类型的函数用于输出,如果需要数值,我只需使用如下内容:

void msgInt(char const * const message, int const &value);
...
void msgInt(char const * const message, int const &value)

  m_screen.lock();
  cout << message << " = " << value << endl;
  m_screen.unlock();

这很简单,对我来说效果很好,但我真的不知道它在技术上是否正确。所以我很高兴听到你的意见。


好吧,我没有读到这个:

我不想搜索每个出现的 std::cout 并添加一行锁定代码。

对不起。但是我希望它可以帮助某人。

【讨论】:

此代码不安全,因为 cout 可能会引发异常,然后您的互斥锁永远不会被解锁。您应该将 std::lock_guard 与互斥锁一起使用,以确保无论何时完成互斥锁都会被释放。【参考方案6】:

可行的解决方案是为每个线程使用一个行缓冲区。您可能会得到交错的行,但不会得到交错的字符。如果将其附加到线程本地存储,则还可以避免锁争用问题。然后,当一行已满时(或在刷新时,如果您愿意),您将其写入标准输出。这最后一个操作当然必须使用锁。你把所有这些都塞进一个流缓冲区,你把它放在 std::cout 和它的原始流缓冲区之间。

这不能解决的问题是格式标志(例如数字的十六进制/十进制/八进制)之类的东西,它们有时会在线程之间渗透,因为它们附加到流中。假设您只是在记录而不将其用于重要数据,这没什么不好。它有助于不专门格式化事物。如果您需要某些数字的十六进制输出,请尝试以下操作:

template<typename integer_type>
std::string hex(integer_type v)

    /* Notes:
    1. using showbase would still not show the 0x for a zero
    2. using (v + 0) converts an unsigned char to a type
       that is recognized as integer instead of as character */
    std::stringstream s;
    s << "0x" << std::setfill('0') << std::hex
        << std::setw(2 * sizeof v) << (v + 0);
    return s.str();

类似的方法也适用于其他格式。

【讨论】:

行的内容不是先写入公共缓冲区吗?如果是这样,则行缓冲打印根本不是线程安全的! 我不太清楚你的意思,正如我在第一层所说的,每个线程都有自己的缓冲区,所以没有公共缓冲区。在第二层,确实有一个公共缓冲区,但正如我上面写的那样,该缓冲区“必须使用锁”。两者对我来说都是线程安全的。 抱歉没有通读。我的习惯是阅读前几句话并查看代码示例,我没有发现那里有任何锁。我倾向于将开头或结尾视为摘要。而且我认为适合快速浏览的答案真的很棒。 @UlrichEckhardt template &lt;integer_type&gt; 是什么意思?代码按原样有效,但没有多大意义;)【参考方案7】:

按照 Conchylicultor 建议的答案,但没有从 std::ostringstream 继承:


编辑:修复了重载运算符的返回类型,并为 std::endl 添加了重载。


编辑 1:我已将其扩展为 simple header-only library,用于记录/调试多线程程序。


#include <iostream>
#include <mutex>
#include <thread>
#include <vector>    
#include <chrono>

static std::mutex mtx_cout;

// Asynchronous output
struct acout

        std::unique_lock<std::mutex> lk;
        acout()
            :
              lk(std::unique_lock<std::mutex>(mtx_cout))
        

        

        template<typename T>
        acout& operator<<(const T& _t)
        
            std::cout << _t;
            return *this;
        

        acout& operator<<(std::ostream& (*fp)(std::ostream&))
        
            std::cout << fp;
            return *this;
        
;

int main(void)



    std::vector<std::thread> workers_cout;
    std::vector<std::thread> workers_acout;

    size_t worker(0);
    size_t threads(5);


    std::cout << "With std::cout:" << std::endl;

    for (size_t i = 0; i < threads; ++i)
    
        workers_cout.emplace_back([&]
        
            std::cout << "\tThis is worker " << ++worker << " in thread "
                      << std::this_thread::get_id() << std::endl;
        );
    
    for (auto& w : workers_cout)
    
        w.join();
    

    worker = 0;

    std::this_thread::sleep_for(std::chrono::seconds(2));

    std::cout << "\nWith acout():" << std::endl;

    for (size_t i = 0; i < threads; ++i)
    
        workers_acout.emplace_back([&]
        
            acout() << "\tThis is worker " << ++worker << " in thread "
                    << std::this_thread::get_id() << std::endl;
        );
    
    for (auto& w : workers_acout)
    
        w.join();
    

    return 0;

输出:

With std::cout:
        This is worker 1 in thread 139911511856896
        This is worker  This is worker 3 in thread 139911495071488
        This is worker 4 in thread 139911486678784
2 in thread     This is worker 5 in thread 139911503464192139911478286080


With acout():
        This is worker 1 in thread 139911478286080
        This is worker 2 in thread 139911486678784
        This is worker 3 in thread 139911495071488
        This is worker 4 in thread 139911503464192
        This is worker 5 in thread 139911511856896

【讨论】:

@vallismortis 完成。我希望你觉得它有用。【参考方案8】:

我知道这是一个老问题,但它对我的问题帮助很大。我根据这篇帖子的答案创建了一个实用程序类,我想分享我的结果。

考虑到我们使用 C++11 或更高版本的 C++,这个类提供了 print 和 println 函数来在调用标准输出流之前组合字符串,避免并发问题。这些是可变参数函数,它们使用模板来打印不同的数据类型。

您可以在我的 github 上查看它在生产者-消费者问题中的用途:https://github.com/eloiluiz/threadsBar

所以,这是我的代码:

class Console 
private:
    Console() = default;

    inline static void innerPrint(std::ostream &stream) 

    template<typename Head, typename... Tail>
    inline static void innerPrint(std::ostream &stream, Head const head, Tail const ...tail) 
        stream << head;
        innerPrint(stream, tail...);
    

public:
    template<typename Head, typename... Tail>
    inline static void print(Head const head, Tail const ...tail) 
        // Create a stream buffer
        std::stringbuf buffer;
        std::ostream stream(&buffer);
        // Feed input parameters to the stream object
        innerPrint(stream, head, tail...);
        // Print into console and flush
        std::cout << buffer.str();
    

    template<typename Head, typename... Tail>
    inline static void println(Head const head, Tail const ...tail) 
        print(head, tail..., "\n");
    
;

【讨论】:

【参考方案9】:

这就是我使用自定义枚举和宏在std::cout 上管理线程安全操作的方式:

enum SynchronisedOutput  IO_Lock, IO_Unlock ;

inline std::ostream & operator<<(std::ostream & os, SynchronisedOutput so) 
  static std::mutex mutex;

  if (IO_Lock == so) mutex.lock();
  else if (IO_Unlock == so)
    mutex.unlock();

  return os;


#define sync_os(Os) (Os) << IO_Lock
#define sync_cout sync_os(std::cout)
#define sync_endl '\n' << IO_Unlock

这让我可以写如下内容:

sync_cout << "Hello, " << name << '!' << sync_endl;

在没有赛车问题的线程中。

【讨论】:

【参考方案10】:

我遇到了和你类似的问题。您可以使用以下类。 这只支持输出到std::cout,但如果你需要一个通用的,请告诉我。在下面的代码中,tsprint 创建了类ThreadSafePrinter 的内联临时对象。如果您愿意,如果您使用了cout 而不是std::cout,您可以将tsprint 更改为cout,这样您就不必替换cout 的任何实例,但我不建议在一般的。无论如何,从项目的开头开始对此类调试行使用特殊的输出符号要好得多。

我也喜欢这个解决方案:1。 在我的解决方案中,所有线程都可以继续插入到它们对应的thread_localstringstream静态对象中,然后仅在析构函数中触发需要刷新时才锁定互斥锁。这有望通过缩短保持互斥锁的持续时间来提高效率。也许我可以包含类似于1 中提到的sync_endl 解决方案的机制。

class ThreadSafePrinter

    static mutex m;
    static thread_local stringstream ss;
public:
    ThreadSafePrinter() = default;
    ~ThreadSafePrinter()
    
        lock_guard  lg(m);
        std::cout << ss.str();
        ss.clear();
    

    template<typename T>
    ThreadSafePrinter& operator << (const T& c)
    
        ss << c;
        return *this;
    


    // this is the type of std::cout
    typedef std::basic_ostream<char, std::char_traits<char> > CoutType;

    // this is the function signature of std::endl
    typedef CoutType& (*StandardEndLine)(CoutType&);

    // define an operator<< to take in std::endl
    ThreadSafePrinter& operator<<(StandardEndLine manip)
    
        manip(ss);
        return *this;
    
;
mutex ThreadSafePrinter::m;
thread_local stringstream ThreadSafePrinter::ss;
#define tsprint ThreadSafePrinter()

void main()

    tsprint << "asd ";
    tsprint << "dfg";

【讨论】:

虽然答案提供了一种解决方法,但从技术上讲,它并不是问题的解决方案。考虑到提议的方法的目的,应该澄清 static 和 thread_local 成员不能在定义提议的类的头文件中实例化。 (见***.com/a/18861043/3434933) @PierreSchroeder 感谢您的反馈!关于您的第一点:是的,这是与此页面上其他人的解决方案一样的解决方法。为了解决第二部分,我只是从我的主文件中复制粘贴代码(请注意,在类的正下方有一个主调用),因此它可以在单个文件中工作。我假设任何提出此类高级问题的人都能够知道他们不应该将静态对象定义放在头文件中。当然,t 并不意味着我的假设普遍成立。另一点:我的解决方案使用线程本地字符串流使锁更短。【参考方案11】:

除了同步之外,此解决方案还提供有关写入日志的线程的信息。

免责声明:这是一种非常简单的日志同步方式,但它可能适用于一些小的调试用例。

thread_local int thread_id = -1;
std::atomic<int> thread_count;

struct CurrentThread 

  static void init() 
    if (thread_id == -1) 
      thread_id = thread_count++;
    
  

  friend std::ostream &operator<<(std::ostream &os, const CurrentThread &t) 
    os << "[Thread-" << thread_id << "] - ";
    return os;
  
;

CurrentThread current_thread;
std::mutex io_lock;
#ifdef DEBUG
#define LOG(x) CurrentThread::init(); std::unique_lock<std::mutex> lk(io_lock); cout << current_thread << x << endl;
#else
#define LOG(x)
#endif

可以这样使用。

LOG(cout << "Waiting for some event");

它会给出日志输出

[Thread-1] - Entering critical section 
[Thread-2] - Waiting on mutex
[Thread-1] - Leaving critical section, unlocking the mutex

【讨论】:

以上是关于如何轻松使 std::cout 线程安全?的主要内容,如果未能解决你的问题,请参考以下文章

如何使静态日历线程安全

如何使 ObservableCollection 线程安全?

如何使 Stack.Pop 线程安全

如何使这个线程安全

如何使 Swift 类单例实例线程安全?

如何使用 Platform.runLater 使 JavaFX 线程安全