我对线程安全日志类的方法是否糟糕?

时间:2013-10-22 13:40:48

标签: c++ logging thread-safety iostream manipulators

我一直在寻找解决线程安全日志问题的各种方法,但是我还没有看到任何类似这样的东西,所以我不知道是否因为成为一个完整的新手而没有注意到它是多么可怕C ++,线程和iostream。它似乎适用于我已经完成的基本测试。

基本上我有一个Log类(有创意,我知道......),它有运算符<<为标准操纵者设置,所以我可以愉快地传递任何我想要的东西。

但是,我知道像:

std::cout << "Threads" << " will" << " mess" << " with" << "this." << std::endl;
当多个线程写入cout(或Log ostream指向的任何地方)时,

可能会交错。所以,我创建了一些特定于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

与制作自定义操纵器相关的位被here无耻地抄袭,但不要因为我不称职的copypasta而责怪他们。

3 个答案:

答案 0 :(得分:4)

这是一个坏主意。 想象一下:

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;
}

您需要调整lockunlock方法以获得签名void lock()void unlock(),并使其成为班级Log的成员函数


另一方面,这相当笨重。请注意,在C ++ 11中,使用std::cout是线程安全的。所以你可以很容易地做到

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

完全没有额外的锁。

答案 1 :(得分:3)

您不需要显式传递锁定操纵器,您可以使用哨兵(使用RAII语义,如Hans Passant所说)

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<<调用转发给父Log实例...
  3. 然后在表达式结尾处超出范围(或者如果foo抛出)
    • 解锁Log对象

  4. 虽然这是安全的,但它也会产生很多争用(在格式化消息时,互斥锁的锁定时间比我通常喜欢的时间长)。较低争用的方法是将格式化为本地存储(线程本地或作用域本地)而根本不进行锁定,然后保持锁定足够长的时间以将其移动到共享日志记录队列中。

答案 2 :(得分:2)

这不是一个非常好的主意,因为有人会致命 在某些时候忘记unlock,导致所有线程 挂在下一个日志。还有如果会发生什么的问题 您正在记录的表达式之一。 (不应该 因为您不希望在日志中有实际行为 声明,没有任何行为的事情不应该 扔。但你永远不知道。)

日志记录的常用解决方案是使用特殊的临时解决方案 object,它在其构造函数中获取锁,并释放它 析构函数(并且还执行刷新,并确保存在 尾随'\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的静态实例进行分配 函数来启动日志记录,并结束它,但确实如此 有点复杂。)