繁体   English   中英

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

[英]How to easily make std::cout thread-safe?

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

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

有什么更好的做法吗?

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

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

字符串出现乱码的原因是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();

这样,您的字符串就不会出现乱码,因为它们已经完全成型,而且在发送它们之前无论如何都要完全成型您的字符串也是一种更好的做法。

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

我想您可以实现自己的类,该类包装cout并将互斥体与其关联。 那个新类的operator <<会做三件事:

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

这个不同的类会将锁和委托操作符<<保留到包装的流中。 第二个类的析构函数最终会破坏锁并释放互斥锁。

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

我们locked_ostream两个类称为synchronized_ostreamlocked_ostream 如果sync_cout是包装std::coutsynchronized_ostream的实例,则序列

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

将导致以下操作:

  1. synchronized_ostream::operator<<将获得锁
  2. synchronized_ostream::operator<<会将“Hello,”的打印委托给cout
  3. operator<<(std::ostream&, const char*)将打印“Hello,”
  4. synchronized_ostream::operator<<将构造一个locked_ostream并将锁传递给它
  5. locked_ostream::operator<<会将name的打印委托给cout
  6. operator<<(std::ostream&, std::string)将打印名称
  7. 感叹号和端线操纵器发生相同的cout委托
  8. locked_ostream临时被破坏,锁被释放

我真的很喜欢 Nicolás 在这个问题中给出的技巧,即创建一个临时对象并将保护代码放在析构函数上。

/** 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收集。 一旦达到昏迷,对象就会被销毁并刷新所有收集的信息。

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

为了快速调试 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 并添加一行锁定代码。

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

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

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

沿着 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;
}

输出:

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

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

考虑到我们使用的是 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");
    }
};

这就是我使用自定义枚举和宏在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;

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM