Как легко сделать std:: cout потокобезопасным?
У меня есть многопоточное приложение, которое интенсивно использует std::cout
для регистрации без какой-либо блокировки. В таком случае, как я могу легко добавить механизм блокировки, чтобы сделать потокобезопасным std::cout
?
Я не хочу искать каждое вхождение std::cout
и добавлять строку кода блокировки. Это слишком утомительно.
Любая лучшая практика?
Ответы
Ответ 1
Примечание. Этот ответ до С++ 20, поэтому он не использует std::osyncstream
с отдельной буферизацией, а вместо этого использует блокировку.
Я полагаю, вы могли бы реализовать свой собственный класс, который упаковывает cout
и ассоциирует с ним мьютекс. operator <<
этого нового класса будет делать три вещи:
- создать блокировку для мьютекса, возможно, блокируя другие потоки
- сделать вывод, т.е. сделать оператор
<<
для обернутого потока и переданного аргумента - создать экземпляр другого класса, передав блокировку этому
Этот другой класс будет хранить оператор блокировки и делегирования <<
в обернутом потоке. Деструктор этого второго класса в конечном итоге разрушит замок и освободит мьютекс.
Таким образом, любой вывод, который вы записываете как один оператор, то есть как одна последовательность <<
вызовов, будет печататься атомарно, пока весь ваш вывод проходит через этот объект с одним и тем же мьютексом.
Позвольте вызвать два класса synchronized_ostream
и locked_ostream
. Если sync_cout
является экземпляром synchronized_ostream
который упаковывает std::cout
, то последовательность
sync_cout << "Hello, " << name << "!" << std::endl;
приведет к следующим действиям:
-
synchronized_ostream::operator<<
получит блокировку -
synchronized_ostream::operator<<
делегировал бы печать "Hello" cout
-
operator<<(std::ostream&, const char*)
выведет "Hello" -
synchronized_ostream::operator<<
создаст locked_ostream
и передаст блокировку этому -
locked_ostream::operator<<
делегировал бы печать name
cout
-
operator<<(std::ostream&, std::string)
выведет имя - То же самое делегирование
cout
происходит с восклицательным знаком и конечным манипулятором - Временный
locked_ostream
разрушается, блокировка locked_ostream
Ответ 2
Хотя я не могу быть уверен, что это относится ко всем компиляторам/версиям std libs
но в базе кода я использую std:: cout:: operator < (lt)() он уже потокобезопасен.
Я предполагаю, что то, что вы действительно пытаетесь сделать, останавливает 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
Если в этом случае есть гораздо более простой ответ, чем создание собственного потокового безопасного 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();
Таким образом, ваши жало не могут быть искажены, потому что они уже полностью сформированы, а также лучшая практика, чтобы полностью сформировать ваши строки, прежде чем отправлять их.
Ответ 3
Мне очень нравится трюк от 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
. Как только кома будет достигнута, объект будет уничтожен и очистит всю собранную информацию.
Ответ 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.
Ответ 5
Реальное решение использует линейный буфер для каждого потока. Вы можете получить чередующиеся строки, но не чередующиеся символы. Если вы присоедините это к локальному хранилищу потока, вы также избежите проблем с блокировкой. Затем, когда строка заполнена (или, если хотите, сброшена), вы записываете ее в стандартный вывод. Эта последняя операция, конечно, должна использовать блокировку. Вы помещаете все это в потоковый буфер, который вы помещаете между std :: cout и его оригинальным потоковым буфером.
Проблема, которую не удается решить, это такие вещи, как флаги формата (например, hex/dec/oct для чисел), которые иногда могут просачиваться между потоками, поскольку они прикреплены к потоку. В этом нет ничего плохого, если вы только регистрируетесь и не используете его для важных данных. Это помогает просто не форматировать вещи специально. Если вам нужен шестнадцатеричный вывод для определенных чисел, попробуйте это:
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();
}
Подобные подходы работают и для других форматов.
Ответ 6
Для быстрой отладки приложений С++ 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 и добавлять строку кода блокировки.
Прости. Однако я надеюсь, что это поможет кому-то.
Ответ 7
В соответствии с ответом, предложенным Conchylicultor, но без наследования от std::ostringstream
:
EDIT: исправлен тип возвращаемого значения для перегруженного оператора и добавлена перегрузка для 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
Ответ 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
Помимо синхронизации это решение предоставляет информацию о потоке, из которого был записан журнал.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Это довольно наивный способ синхронизации журналов, однако он может быть применим для некоторых небольших случаев использования для отладки.
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