Синхронизировать запись для входа в многопоточный процесс
Я реализовал Logger, поэтому его можно использовать как ostream. например, если кто-то хочет написать в журнал - он может сделать что-то вроде этого:
LOG << "hello world " << 6 << 8.6 << "\n";
Журнал будет записан на экран, в файл журнала и в любой другой источник, который нравится пользователю (это не проблема).
Для достижения этой цели я создал макрос для LOG:
#define LOG Logger::GetInstance()(__FILENAME__, __func__, __LINE__)
и перегружены operator () и operator <<:
template <typename T>
inline Logger& operator << (const T& msg) {
std::stringstream ss;
ss << msg;
PrintToFile(ss.str());
PrintToScreen(ss.str());
return *this;
}
Logger& Logger::operator () (const std::string& sourceFile, const std::string& funcName, int lineNumber) {
std::stringstream ss;
ss << Utilities::GetFormattedTime("%d.%m.%y %H:%M:%S") << "::" << sourceFile << "(" << lineNumber << ")::" <<
funcName << "::";
PrintToFile(level, ss.str());
PrintToScreen(level, ss.str());
return *this;
}
Проблема в том, что когда у меня в процессе запущено несколько потоков, печать может обрезаться из-за переключения контекста между потоками в середине строки примера выше (LOG << "hello world... ")
Использование мьютекса может не помочь, потому что operator << и operator () не являются одной и той же функцией.
Есть ли блестящее или простое решение этой проблемы?
5 ответов
Просто из головы. Если вы хотите сохранить свой подход с операторами потокового ввода-вывода как есть, вы можете использовать своего рода прокси-объект, который блокирует \ разблокирует мьютекс.
Пожалуйста, не обращайте внимания на стиль кодирования (особенно на swfull и опасную реализацию Logger). Ниже вы можете найти краткую иллюстрацию упомянутой идеи.
template<class TLogger, class TLockObject>
class LoggerProxy
{
public:
LoggerProxy(TLogger &logger, TLockObject &lockObj)
: m_logger(logger),
m_lockGuard(lockObj)
{
}
template <typename T>
inline LoggerProxy& operator << (const T& msg)
{
m_logger.Write(msg);
return *this;
}
private:
TLogger & m_logger;
std::lock_guard<typename TLockObject> m_lockGuard;
};
//Purpose of code below is just an illustration of usage LoggerProxy class. Don't use it in production code.
class Logger
{
public:
static Logger& GetInstance()
{
static Logger instance;
return instance;
}
static std::mutex& GetLockObject()
{
static std::mutex lockObj;
return lockObj;
}
template <typename T>
inline void Write (const T& msg) {
std::cout << msg << std::endl;
}
};
#define LOG LoggerProxy<Logger, std::mutex>(Logger::GetInstance(), Logger::GetLockObject())
int main()
{
LOG << 10 << "HELLO" << 1.1;
LOG << 101 << "HELLO2" << 11.1;
return 0;
}
Делитесь меньше, блокируйте меньше.
Если вы собираетесь синхронизировать на каждом operator<<
у вас будет очень плохая производительность, когда ваше приложение скомпилировано с включенным ведением журнала.
Это набросок того, как я это сделаю:
Концепт
namespace Logging {
struct SinkConcept { void commit(std::string const&); }; // documentation only
Давайте разработаем узкий контракт, в котором у любого приемника есть только один метод.
Теперь давайте создадим LogTx
- представление лог-транзакции.
LogTx
должен быть временным объектом, учитывающим перемещение, создает сообщение журнала локально. Это означает, что буферы не являются общими и не нуждаются в синхронизации до тех пор, пока вы не подтвердите.
Фиксация делается из деструктора:
// movable log transaction (using arbitrary sink)
template <typename Sink> struct LogTx {
LogTx(Sink& s) : _sink(s) {}
LogTx(LogTx&&) = default;
unique_flag _armed;
std::ostringstream _oss;
Sink& _sink;
~LogTx() { if (_armed) _sink.commit(_oss.str()); }
template <typename T> LogTx& operator<<(T&& rhs)& { return (_oss << rhs), *this; }
template <typename T> LogTx operator<<(T&& rhs)&& { return (_oss << rhs), std::move(*this); }
};
Это все. _armed
флаг гарантирует, что деструктор не фиксируется в перемещенных экземплярах.
Некоторые образцы Раковины
Теперь давайте добавим простые приемники, чтобы мы могли сделать демонстрацию. Начнем с самого простого:
struct NullSink { void commit(std::string const&) const {} };
А теперь давайте полезнее. Приемник для фиксации транзакций журнала в любом ostream
объект или ссылка:
template <typename Impl, bool Flush = true>
struct StreamSink {
StreamSink(Impl stream_or_ref = {}) : _sor(std::move(stream_or_ref)) {}
StreamSink(StreamSink&& rhs) : StreamSink(std::move(rhs._sor)) {}
void commit(std::string const& msg) {
std::lock_guard<std::mutex> lk(_mx);
get() << msg << "\n";
if (Flush) get() << std::flush;
}
std::ostream& get() { return _sor; }
private:
mutable std::mutex _mx;
Impl _sor; // stream convertible to ostream&
};
И потому, что вы писали в несколько мест назначения в вашем примере:
template <typename A, typename B> struct TeeSink { // dispatch to two sinks
A a; B b;
void commit(std::string const& msg) { a.commit(msg); b.commit(msg); }
};
Некоторые удобные функции
Если вы не используете C++17, некоторые фабричные функции будут приветствоваться.
// factory functions (mostly not needed in c++17 with deduction guides)
template <typename A, typename B>
TeeSink<A, B> tee(A&& a, B&& b) { return { std::forward<A>(a), std::forward<B>(b) }; }
StreamSink<std::ofstream, false> log_to(std::ofstream&& file) { return {std::move(file)}; }
StreamSink<std::reference_wrapper<std::ostream>, true> log_to(std::ostream& os) { return {os}; }
Давайте также добавим глобальные экземпляры приемников для стандартных потоков, чтобы вы могли использовать их для получения одинаковой синхронизации везде:
auto& log_to_stderr() {
static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cerr) };
return s_instance;
}
auto& log_to_stdout() {
static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cout) };
return s_instance;
}
auto& null_sink() {
static NullSink const s_instance{};
return s_instance;
}
template <typename Sink>
LogTx<Sink> make_tx(Sink& sink) { return {sink}; }
И наконец, Пьес де Сопротивление: makeTx
создать LogTx
для данной раковины:
template <typename Sink>
LogTx<Sink> make_tx(Sink& sink) { return {sink}; }
ДЕМО ВРЕМЯ
Теперь мы можем собрать это вместе:
#define LOG_TO(sink) (Logging::make_tx(sink) << __FILE__ << ":" << __LINE__ << "\t" << __func__ << "\t")
#ifdef NOLOGGING
#define LOG LOG_TO(Logging::null_sink())
#else
static auto _file_sink = Logging::log_to(std::ofstream("demo.log"));
static auto _both_sink = tee(_file_sink, Logging::log_to_stderr());
#define LOG LOG_TO(_both_sink)
#endif
Это в значительной степени то, что вы хотели:
#include <thread>
void worker(std::string id) {
while (auto r = rand()%10) {
std::this_thread::sleep_for(std::chrono::milliseconds(r));
LOG << "Ping from " << id;
}
}
int main() {
LOG << "Hello";
{
std::thread a(worker, "A"), b(worker, "B");
a.join();
b.join();
}
LOG << "Bye";
}
Отпечатки, как на stderr, так и на demo.log
:
main.cpp:104 main Hello
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from B
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from B
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from B
main.cpp:99 worker Ping from B
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from B
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from A
main.cpp:99 worker Ping from A
main.cpp:110 main Bye
Полный список C++11
Добавлена совместимая с C++11 версия, в которую я включил полный список для защиты от гниения ссылок:
[C++11 Live On Coliru] [ http://coliru.stacked-crooked.com/a/6360aad26b037df2)
#include <functional> // for std::reference_wrapper
#include <iostream>
#include <sstream>
#include <fstream>
#include <mutex>
namespace Logging {
// utility to safely implement movable log transactions
struct unique_flag {
bool value = true;
unique_flag() = default;
unique_flag(unique_flag&& rhs) : value(rhs.value) { rhs.value = false; }
operator bool() const { return value; }
};
struct SinkConcept { void commit(std::string const&); }; // documentation only
// movable log transaction (using arbitrary sink)
template <typename Sink> struct LogTx {
LogTx(Sink& s) : _sink(s) {}
LogTx(LogTx&&) = default;
unique_flag _armed;
std::ostringstream _oss;
Sink& _sink;
~LogTx() { if (_armed) _sink.commit(_oss.str()); }
template <typename T> LogTx& operator<<(T&& rhs)& { return (_oss << rhs), *this; }
template <typename T> LogTx operator<<(T&& rhs)&& { return (_oss << rhs), std::move(*this); }
};
// Some sink models
struct NullSink { void commit(std::string const&) const {} };
template <typename Impl, bool Flush = true>
struct StreamSink {
StreamSink(Impl stream_or_ref = {}) : _sor(std::move(stream_or_ref)) {}
StreamSink(StreamSink&& rhs) : StreamSink(std::move(rhs._sor)) {}
void commit(std::string const& msg) {
std::lock_guard<std::mutex> lk(_mx);
get() << std::move(msg);
if (Flush)
get() << std::endl;
else
get() << "\n";
}
std::ostream& get() { return _sor; }
private:
mutable std::mutex _mx;
Impl _sor; // stream convertible to ostream&
};
template <typename A, typename B> struct TeeSink { // dispatch to two sinks
A a; B b;
void commit(std::string const& msg) { a.commit(msg); b.commit(msg); }
};
// factory functions (mostly not needed in c++17 with deduction guides)
template <typename A, typename B>
TeeSink<A, B> tee(A&& a, B&& b) { return { std::forward<A>(a), std::forward<B>(b) }; }
StreamSink<std::ofstream, false> log_to(std::ofstream&& file) { return {std::move(file)}; }
StreamSink<std::reference_wrapper<std::ostream>, true> log_to(std::ostream& os) { return {os}; }
StreamSink<std::reference_wrapper<std::ostream>, true>& log_to_stderr() {
static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cerr) };
return s_instance;
}
StreamSink<std::reference_wrapper<std::ostream>, true>& log_to_stdout() {
static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cout) };
return s_instance;
}
NullSink const& null_sink() {
static NullSink const s_instance{};
return s_instance;
}
template <typename Sink>
LogTx<Sink> make_tx(Sink& sink) { return {sink}; }
}
#define LOG_TO(sink) (Logging::make_tx(sink) << __FILE__ << ":" << __LINE__ << "\t" << __func__ << "\t")
#ifdef NOLOGGING
#define LOG LOG_TO(Logging::null_sink())
#else
static auto _file_sink = Logging::log_to(std::ofstream("demo.log"));
static auto _both_sink = tee(_file_sink, Logging::log_to_stderr());
#define LOG LOG_TO(_both_sink)
#endif
#include <thread>
void worker(std::string id) {
while (auto r = rand()%10) {
std::this_thread::sleep_for(std::chrono::milliseconds(r));
LOG << "Ping from " << id;
}
}
int main() {
LOG << "Hello";
{
std::thread a(worker, "A"), b(worker, "B");
a.join();
b.join();
}
LOG << "Bye";
}
Вы правы в том, что operator<<
а также operator ()
это две разные функции, но это не значит, что вы не можете использовать мьютекс.
Из того, что я могу собрать из вашего примера в обоих operator<<
а также operator()
Вы вызываете в конце те же две функции PrintToFile
а также PrintToScreen
, Это заставляет меня думать, что поток, общий для всех потоков, находится в этих двух функциях, а не в вашей operator<<
а также operator()
Таким образом, вы можете заблокировать мьютекс внутри этих функций и иметь потокобезопасный доступ к логгеру. При этом остается только решить, нужно ли вам два мьютекса или только один, это зависит от того, хотите ли вы, чтобы регистрация была "атомарной" как операция, или вы хотите разделить ее.
В "атомарной" версии вы должны использовать одну версию, которая содержит один мьютекс, при записи как на экран, так и в файл, таким образом, ваши журналы будут выполняться последовательно.
Вместо этого, если вы разделили версию, вы хотите иметь две разные функции с двумя разными мьютексами, одну для ведения журнала файлов и одну для входа в экран, этот поток, который хочет записать в файл, не должен ждать потоков, которые уже записали в файл и теперь пишем на экран. Как всегда помните, что наличие двух мьютексов стоило.
Первое ощущение, что operator()
а также operator<<
не связаны. Для этого лучше использовать два мьютекса, один в PrintToFile
и один в PrintToScreen
,
Но вы также можете использовать один и тот же мьютекс для регистрации обоих ваших операторных методов. Поскольку первый оператор является функцией шаблона, у нас есть не две, а n функций. Каждый экземпляр шаблона является дополнительным. Так что использование единого мьютекса для всех функций сделало именно то, что вы хотите.
Если ваш Logger - это класс, а операторы - члены, вы просто делаете свой мьютекс переменной-членом, которую можно (не) блокировать в каждом из ваших методов.
Использовать std::mutex
и заблокировать при первом использовании operator()
или же operator<<
, Затем разблокируйте, если строка пользователя заканчивается \n
, Я предполагаю, что вы уверены, что пользователь заканчивает каждую запись в журнале переводом строки.
class Logger {
std::mutex mux;
//...
public:
Logger& operator()(/*...*/) {
mux.lock();
// write to log
}
Logger& operator<<(const string& str) {
// write to log
if(str[str.size()-1] == '\n')
mux.unlock();
}
//...
};