46

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

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

有更好的做法吗?

4

11 回答 11

42

虽然我不能确定这适用于 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();

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

于 2014-11-13T12:46:31.067 回答
23

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

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

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

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

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

让我们称这两个类synchronized_ostreamlocked_ostream. 如果sync_coutsynchronized_ostreamwraps的一个实例std::cout,则序列

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

将导致以下操作:

  1. synchronized_ostream::operator<<将获得锁
  2. synchronized_ostream::operator<<将“Hello”的打印委托给cout
  3. operator<<(std::ostream&, const char*)会打印“你好,”
  4. synchronized_ostream::operator<<将构造 alocked_ostream并将锁传递给那个
  5. locked_ostream::operator<<将委托name印刷cout
  6. operator<<(std::ostream&, std::string)会打印名字
  7. cout感叹号和端线操纵器发生相同的委托
  8. 临时对象被破坏,locked_ostream锁被释放
于 2013-02-05T23:23:57.433 回答
20

由于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 )将没有数据竞争,并且不会以任何方式交错或乱码,只要每次写入该 final目标缓冲区是通过 std::basic_osyncstream 的(可能不同的)实例创建的。

或者,您可以使用临时:

std::osyncstream(std::cout) << "Hello, " << "World!" << '\n';
于 2017-11-24T22:39:03.190 回答
17

我真的很喜欢 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。一旦达到昏迷状态,对象就会被销毁并刷新所有收集到的信息。

于 2017-01-11T23:47:08.533 回答
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 并添加一行锁定代码。

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

于 2015-03-14T12:50:01.587 回答
4

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

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

于 2013-02-06T21:52:03.197 回答
4

按照 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
于 2017-07-12T00:15:27.770 回答
1

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

考虑到我们使用 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");
    }
};
于 2018-05-27T01:49:38.723 回答
0

这就是我在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;

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

于 2021-01-15T15:46:10.343 回答
0

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

我也喜欢这个解决方案:1。在我的解决方案中,所有线程都可以继续插入到它们相应thread_local stringstream的静态对象中,然后仅在需要刷新时才锁定互斥锁,这在析构函数中触发。这有望通过缩短互斥锁的持有时间来提高效率。也许我可以包含一个类似于1sync_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";
}
于 2021-11-08T14:05:23.790 回答
-1

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

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

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
于 2018-12-08T13:25:02.527 回答