Мой подход к потокобезопасному классу журнала ужасен?

Я смотрел вокруг на различные подходы к проблеме потокового ведения журнала, но я не видел ничего подобного, поэтому я не знаю, ужасно ли это, что я не заметил, потому что я новичок в C ++, потоках и iostreams. Кажется, он работает в базовых тестах, через которые я прошел.

В основном у меня есть класс Log (креатив, я знаю …), у которого есть оператор<< настроить для стандартных манипуляторов, так что я могу весело передать все, что я хочу.

Тем не менее, я знаю, что-то вроде:

std::cout << "Threads" << " will" << " mess" << " with" << "this." << std::endl;

будет потенциально чередоваться, когда несколько потоков записывают в cout (или куда бы ни направлялся поток o Log). Итак, я создал некоторые манипуляторы, специфичные для класса Log, которые позволяют мне делать это:

Log::log << lock << "Write" << " what" << " I" << " want" << std::endl << unlock;

Я просто хочу знать, является ли это по своей сути ужасной идеей, имея в виду, что я готов согласиться с тем, что пользователи класса Log должны быть дисциплинированы с помощью ‘lock’ и ‘unlock’. Я подумал о том, чтобы сделать автоматическую разблокировку ‘std :: endl’, но кажется, что это вызовет больше головной боли … Я думаю, что в любом случае в тестировании должно быть недисциплинированное использование, но если кто-то сможет увидеть способ сделать такое использование вызывающим ошибки времени, это было бы хорошо.

Я также был бы признателен за любые предложения, чтобы сделать мой код чище.

Вот урезанная версия класса для демонстрационных целей; у всего этого есть еще несколько конструкторов, принимающих такие вещи, как имена файлов, поэтому они не имеют отношения к вопросу.

#include <iostream>
#include <thread>
#include <fstream>

class Log{
public:
//Constructors
Log(std::ostream & os);
// Destructor
~Log();
// Input Functions
Log & operator<<(const std::string & msg);
Log & operator<<(const int & msg);
Log & operator<<(std::ostream & (*man)(std::ostream &)); // Handles manipulators like endl.
Log & operator<<(std::ios_base & (*man)(std::ios_base &)); // Handles manipulators like hex.
Log & operator<<(Log & (*man)(Log &)); // Handles custom Log manipulators like lock and unlock.
friend Log & lock(Log & log); // Locks the Log for threadsafe output.
friend Log & unlock(Log & log); // Unlocks the Log once threadsafe output is complete.
private:
std::fstream logFile;
std::ostream & logStream;
std::mutex guard;
};

// Log class manipulators.
Log & lock(Log & log); // Locks the Log for threadsafe output.
Log & unlock(Log & log); // Unlocks the Log once threadsafe output is complete.

void threadUnsafeTask(int * input, Log * log);
void threadSafeTask(int * input, Log * log);

int main(){
int one(1), two(2);
Log log(std::cout);
std::thread first(threadUnsafeTask, &one, &log);
std::thread second(threadUnsafeTask, &two, &log);
first.join();
second.join();
std::thread third(threadSafeTask, &one, &log);
std::thread fourth(threadSafeTask, &two, &log);
third.join();
fourth.join();
return 0;
}

void threadUnsafeTask(int * input, Log * log){
*log << "Executing" << " thread '" << *input << "', " << "expecting " << "interruptions " << "frequently." << std::endl;
}

void threadSafeTask(int * input, Log * log){
*log << lock << "Executing" << " thread '" << *input << "', " << "not expecting " << "interruptions." << std::endl << unlock;
}

// Constructors (Most left out as irrelevant)
Log::Log(std::ostream & os): logFile(), logStream(logFile), guard(){
logStream.rdbuf(os.rdbuf());
}

// Destructor
Log::~Log(){
logFile.close();
}

// Output Operators
Log & Log::operator<<(const std::string & msg){
logStream << msg;
return *this;
}

Log & Log::operator<<(const int & msg){
logStream << msg;
return *this;
}

Log & Log::operator<<(std::ostream & (*man)(std::ostream &)){
logStream << man;
return *this;
}

Log & Log::operator<<(std::ios_base & (*man)(std::ios_base &)){
logStream << man;
return *this;
}

Log & Log::operator<<(Log & (*man)(Log &)){
man(*this);
return *this;
}

// Manipulator functions.
Log & lock(Log & log){
log.guard.lock();
return log;
}

Log & unlock(Log & log){
log.guard.unlock();
return log;
}

Он работает для меня на Ubuntu 12.04 g ++, скомпилирован с:

g++ LogThreadTest.cpp -o log -std=c++0x -lpthread

Куски, имеющие отношение к созданию пользовательских манипуляторов, были бесстыдно Вот но не вините их за мою некомпетентную коппасту.

3

Решение

Это плохая идея.
Вообразите это:

void foo()
{
throw std::exception();
}

log << lock << "Write" << foo() << " I" << " want" << std::endl << unlock;
^
exception!

Это оставляет ваш Log заперта. Что плохо, так как другие потоки могут ожидать блокировки.
Это также происходит каждый раз, когда вы просто забывать сделать unlock,
Вы должны использовать RAII здесь:

// just providing a scope
{
std::lock_guard<Log> lock(log);
log << "Write" << foo() << " I" << " want" << std::endl;
}

Вам нужно настроить ваш lock а также unlock методы иметь подписи void lock() а также void unlock() и сделать их членами-функциями класса Log,


С другой стороны, это довольно громоздко. Обратите внимание, что в C ++ 11, используя std::cout потокобезопасен. Так что вы можете легко сделать

std::stringstream stream;
stream << "Write" << foo() << " I" << " want" << std::endl;
std::cout << stream.str();

который полностью свободен от дополнительных замков.

4

Другие решения

Вам не нужно явно передавать манипулятор блокировки, вы можете использовать часовой (с семантикой RAII, как говорит Ханс Пассант)

class Log{
public:
Log(std::ostream & os);
~Log();

class Sentry {
Log &log_;
public:
Sentry(Log &l) log_(l) { log_.lock(); }
~Sentry() { log_.unlock(); }

// Input Functions just forward to log_.logStream
Sentry& operator<<(const std::string & msg);
Sentry& operator<<(const int & msg);
Sentry& operator<<(std::ostream & (*man)(std::ostream &)); // Handles manipulators like endl.
Sentry& operator<<(std::ios_base & (*man)(std::ios_base &)); // Handles manipulators like hex.
};

template <typename T>
Sentry operator<<(T t) { return Sentry(*this) << t; }
void lock();
void unlock();

private:
std::fstream logFile;
std::ostream & logStream;
std::mutex guard;
};

Теперь пишу

Log::log << "Write" << " what" << " I" << " want" << foo() << std::endl;

будут:

  1. создать временный объект Sentry
    • который блокирует объект Log
  2. … вперед каждый operator<< вызов родительского экземпляра журнала …
  3. а затем выходит из области видимости в конце выражения (или если foo броски)
    • который разблокирует объект Log

Хотя это безопасно, это также вызывает много разногласий (мьютекс блокируется дольше, чем обычно, при форматировании сообщения). Подход с меньшим количеством конфликтов будет заключаться в том, чтобы выполнить форматирование в локальное хранилище (локальное для потока или локальное для области) без какой-либо блокировки, а затем удерживать блокировку достаточно долго, чтобы переместить ее в общую очередь журналирования.

3

Это не очень хорошая идея, так как кто-то смертельно
забудь unlock в какой-то момент, вызывая все потоки
повесить на следующий журнал. Существует также проблема того, что произойдет, если
выдает одно из выражений, которое вы регистрируете. (Это не должно
случиться, так как вы не хотите иметь реальное поведение в журнале
утверждение, и вещи, которые не имеют никакого поведения, не должны
бросить. Но вы никогда не знаете.)

Обычное решение для ведения журнала заключается в использовании специального временного
объект, который захватывает замок в своем конструкторе и освобождает его в
деструктор (а также делает сброс, и гарантирует, что есть
отставание '\n'). Это может быть сделано очень элегантно в C ++ 11,
используя семантику перемещения (потому что вы обычно хотите создать
экземпляр временного в функции, но временный, чей
деструктор должен действовать вне функции); в C ++ 03 вы
необходимо разрешить копирование и убедиться, что это только окончательная копия
который снимает блокировку

Грубо говоря, твой Log класс будет выглядеть примерно так:

struct LogData
{
std::unique_lock<std::mutex> myLock
std::ostream myStream;

LogData( std::unique_lock<std::mutex>&& lock,
std::streambuf* logStream )
:  myLock( std::move( lock ) )
,  myStream( logStream )
{
}

~LogData()
{
myStream.flush();
}
};

class Log
{
LogData* myDest;
public:
Log( LogData* dest )
: myDest( dest )
{
}
Log( Log&& other )
: myDest( other.myDest )
{
other.myDest = nullptr;
}
~Log()
{
if ( myDest ) {
delete myDest;
}
}
Log& operator=( Log const& other ) = delete;

template <typename T>
Log& operator<<( T const& obj )
{
if ( myDest != nullptr ) {
myDest->myStream << obj;
}
}
};

(Если ваш компилятор не имеет семантики перемещения, вам придется
подделать это как-то. Если худшее становится худшим, вы можете просто сделать
один указатель член Log mutable, и положить тот же код в
конструктор копирования с традиционной подписью. Некрасиво, но как
обходной путь …)

В этом решении у вас будет функция log, который возвращает
экземпляр этого класса, либо с действительным LogData
(распределяется динамически) или нулевой указатель, в зависимости от того,
регистрация активна или нет. (Можно избежать динамического
распределение, используя статический экземпляр LogData у которого есть
функции, чтобы начать запись журнала и завершить ее, но это
немного сложнее.)

2
По вопросам рекламы [email protected]