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

xml*_*lmx 27 c++ logging multithreading iostream locking

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

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

有更好的做法吗?

小智 25

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

我假设你真正试图做的就是在与多个线程中的每个字符串连接多次运算符时,将std :: cout从混合字符串中停止.

字符串变得混乱的原因是因为操作符上存在"外部"竞争<<这可能导致这样的事情发生.

//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 sure \n
jumped of the lazy dog \n
Run Code Online (Sandbox Code Playgroud)

如果是这种情况,那么答案要比制作自己的线程安全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();
Run Code Online (Sandbox Code Playgroud)

这样你的叮咬就不会因为它们已经完全形成而造成乱码,而且在发送之前无论如何还要更好地完成你的弦乐.

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

MvG*_*MvG 18

我猜你可以实现你自己的类,它将std::osyncstream一个互斥量与它相关联.在cout新课改的会做三件事情:

  1. 为互斥锁创建一个锁,可能会阻塞其他线程
  2. 执行输出,即为operator <<包装流和传递的参数执行运算符
  3. 构造一个不同类的实例,将锁传递给它

这个不同的类会将锁和委托运算符<<保持为包装流.第二类的析构函数最终会破坏锁并释放互斥锁.

因此<<,只要所有输出都使用相同的互斥锁通过该对象,您将作为单个语句写入的任何输出(即作为单个调用序列)将以原子方式打印.

让我们把两个类<<synchronized_ostream.如果locked_ostream是一个sync_cout包装的实例synchronized_ostream,那么序列

sync_cout << "Hello, " << name << "!" << std::endl;
Run Code Online (Sandbox Code Playgroud)

会导致以下操作:

  1. std::cout 会得到锁
  2. synchronized_ostream::operator<< 将"Hello"的印刷委托给 synchronized_ostream::operator<<
  3. cout 会打印"你好",
  4. operator<<(std::ostream&, const char*)会构造一个synchronized_ostream::operator<<并将锁传递给它
  5. locked_ostream委托打印locked_ostream::operator<<name
  6. cout 会打印出这个名字
  7. operator<<(std::ostream&, std::string)对于感叹号和结束操纵器,会发生相同的委托
  8. cout临时得到破坏,锁被释放

  • 这可以简化一点。首先,如果任何代码不遵守规则,它仍然会与 `cout` 混淆,这意味着你必须更改任何出现的 `cout`。在这种情况下,您还可以将其替换为 `(cout_lock(), cout)`,其中 `cout_lock()` 是一种专门用于同步访问 `cout` 的范围锁类型。这将创建一个持续到下一行的临时对象,而表达式`(A, B)` 的结果由于逗号运算符而为 `B`。我声称这是更少的代码,如果你愿意,你仍然可以锁定多于一行的互斥锁。 (2认同)
  • @MvG:他没有使用宏,而是使用逗号运算符。无论如何,这可以简化。将 `cout` 替换为一个缓冲的对象,直到它接收到一个 `'\n'`,然后锁定,将整个缓冲区转储到 `cout`,并立即解锁。 (2认同)

Con*_*tor 11

我真的很喜欢尼古拉斯中给出的伎俩这个问题创建一个临时对象,并把保护代码的析构函数.

/** 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{};
Run Code Online (Sandbox Code Playgroud)

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

PrintThread{} << "my_val=" << val << std::endl;
Run Code Online (Sandbox Code Playgroud)

对象以常规方式收集数据ostringstream.一旦达到昏迷,对象就会被销毁并清除所有收集到的信息.

  • 你必须小心这个想法.不应允许C++中的析构函数抛出异常.默认情况下,在C++ 11中(我推测,超出),如果异常从析构函数传播出来,它将调用std :: terminate. (2认同)
  • @KefSchecter,如果这在实践中确实是一个问题,我想人们可以尝试将析构函数代码包装在一些 `try {} catch(...) {}` 中以消除错误而不是终止。但这确实是该方法的局限性。 (2认同)

can*_*ust 8

沿着 Conchylicultor 建议的答案,但不继承自std::ostringstream


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


编辑1:我已将其扩展为一个简单的仅标头库,用于记录/调试多线程程序。


#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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

  • @vallismortis 完成。希望对你有帮助。 (2认同)

lil*_*zek 8

既然如此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
Run Code Online (Sandbox Code Playgroud)

它保证所有输出到同一个最终目标缓冲区(上例中的std :: cout)都没有数据争用,并且不会以任何方式交错或乱码,只要每次写入最后一个目标缓冲区是通过(可能是不同的)std :: basic_osyncstream实例生成的.

  • @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::cout` 替换为 `std::osyncstream(std::cout)` 可以保证您得到 `A: 0 B: 1` 或 `B: 1 A: 0`。 (2认同)
  • @lilezek你通常*不*想要每个线程有一个“osyncstream”。您需要一个“osyncstream”*每个逻辑消息,该消息不应被另一个线程的文本中断*。 (2认同)

Ulr*_*rdt 5

一个可行的解决方案是为每个线程使用行缓冲区。您可能会得到交错的行,但不会得到交错的字符。如果将其附加到线程本地存储,还可以避免锁争用问题。然后,当一行已满时(或者如果需要,则在刷新时),将其写入标准输出。最后一个操作当然必须使用锁。您将所有这些内容填充到流缓冲区中,并将其放在 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();
}
Run Code Online (Sandbox Code Playgroud)

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


Ger*_*inx 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();
}
Run Code Online (Sandbox Code Playgroud)

我将这些类型的函数用于输出,如果需要数值,我只使用这样的东西:

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();
}
Run Code Online (Sandbox Code Playgroud)

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


好吧,我没有读到这个:

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

抱歉。不过我希望它可以帮助某人。

  • 这段代码不安全,因为 cout 可能会抛出异常,然后您的互斥锁永远不会被解锁。您应该将 std::lock_guard 与互斥锁一起使用,以确保无论何时完成互斥锁都会被释放。 (5认同)