簡體   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