首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >我使用线程安全日志类的方法很糟糕吗?

我使用线程安全日志类的方法很糟糕吗?
EN

Stack Overflow用户
提问于 2013-10-22 13:40:48
回答 3查看 423关注 0票数 4

我一直在研究解决线程安全日志问题的各种方法,但我还没有看到类似的情况,所以我不知道是否因为我是C++、线程和iostreams的完全新手而没有注意到这一点。它似乎在我已经通过的基本测试中起作用。

基本上我有一个日志类(创意,我知道.)这是operator<<为标准操纵者设置的,这样我就可以随意地通过。

然而,我意识到这样的情况:

代码语言:javascript
运行
复制
std::cout << "Threads" << " will" << " mess" << " with" << "this." << std::endl;

当多个线程写入cout (或日志流所指向的任何位置)时,可能会被交织。因此,我创建了一些特定于Log类的操作器,允许我这样做:

代码语言:javascript
运行
复制
Log::log << lock << "Write" << " what" << " I" << " want" << std::endl << unlock;

我只想知道这是否是一个固有的可怕的想法,记住,我愿意接受,用户的日志类将需要纪律‘锁’和‘解锁’。我考虑让'std::endl‘自动解锁,但这似乎会造成更多的头痛.我认为,无论如何,在测试中应该会出现不规范的使用,但是如果有人能够找到一种方法来使这种使用导致编译时错误,那就太好了。

我也希望有任何建议,使我的代码更加清晰。

为了演示起见,下面是这个类的精简版本;整个过程中还有几个构造函数使用文件名之类的东西,所以与这个问题没有什么关系。

代码语言:javascript
运行
复制
#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;
}

它在Ubuntu12.04 g++上为我工作,编译时:

代码语言:javascript
运行
复制
g++ LogThreadTest.cpp -o log -std=c++0x -lpthread

与制造定制操纵器相关的部分是从这里中无耻地抄袭出来的,但不要因为我的不称职的复制体而责怪他们。

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2013-10-22 14:10:06

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

代码语言:javascript
运行
复制
void foo()
{
    throw std::exception();
}

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

这使您的Log被锁定。这是不好的,因为其他线程可能正在等待锁定。每当您忘记执行unlock时,也会发生这种情况。你应该在这里使用RAII:

代码语言:javascript
运行
复制
// 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是线程安全的。所以你可以很容易地

代码语言:javascript
运行
复制
std::stringstream stream;
stream << "Write" << foo() << " I" << " want" << std::endl;
std::cout << stream.str();

完全没有额外的锁。

票数 4
EN

Stack Overflow用户

发布于 2013-10-22 14:16:46

您不需要显式地传递锁机械手,您可以使用哨兵(如Hans所说,带有RAII语义)。

代码语言:javascript
运行
复制
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;
};

现在,写

代码语言:javascript
运行
复制
Log::log << "Write" << " what" << " I" << " want" << foo() << std::endl;

威尔:

  1. 创建临时Sentry对象
    • 哪个锁了Log对象

  1. ..。将每个operator<<调用转发给父日志实例..。
  2. 然后超出表达式末尾的范围(或者如果foo抛出)
    • ,它解锁Log对象

虽然这是安全的,但它也会造成很多争用(在格式化消息时,互斥锁的时间比我通常希望的长)。一种低争用的方法是在没有锁定的情况下将格式设置到本地存储(线程-本地或作用域-本地),然后保持锁足够长的时间将其移动到共享日志队列中。

票数 3
EN

Stack Overflow用户

发布于 2013-10-22 14:38:09

这并不是一个好主意,因为总有一天有人会忘记unlock,导致所有线程挂在下一个日志中。还有一个问题是,如果日志记录中的一个表达式抛出,会发生什么情况。(这不应该发生,因为您不希望日志语句中有实际的行为,没有任何行为的东西不应该抛出。但你永远不会知道。)

日志记录的通常解决方案是使用一个特殊的临时对象,它在构造函数中获取锁,并在析构函数中释放锁(还可以进行刷新,并确保有一个尾随的'\n')。这可以在C++11中非常优雅地使用移动语义(因为您通常希望在函数中创建临时实例,但其析构函数应该在函数之外执行);在C++03中,您需要允许复制,并确保它只是释放锁的最终副本。

粗略地说,您的Log类看起来如下所示:

代码语言:javascript
运行
复制
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的单个指针成员可变,并将相同的代码放入具有传统签名的复制构造函数中。丑陋,但作为一个工作.)

在这个解决方案中,您将有一个函数log,它返回这个类的一个实例,其中包含一个有效的LogData (动态分配),或者一个空指针,这取决于日志记录是否是活动的。(可以通过使用LogData的静态实例来避免动态分配,该实例具有启动日志记录和结束日志记录的功能,但它要复杂一些。)

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/19519590

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档