2013-10-22 42 views
3

我一直在四处寻找各种方法来解决线程安全日志问题,但我还没有看到任何相当类似的东西,所以我不知道是否因为作为C++的一个完整的新手而没有注意到它是某种可怕的东西,线程和iostreams。它似乎在我已经完成的基本测试中起作用。我的线程安全日志类的方法很糟糕吗?

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

但是,我知道是这样的:

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

将有可能得到交错当多个线程编写来清点(或其他地方登录的ostream点)。所以,我创建了一些特定于日志类操纵,让我做到这一点:

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

我只是想知道,如果这是一个固有的可怕想法,记住,我愿意接受轴承用户的Log类需要通过'锁定'和'解锁'进行处理。我认为使'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; 
} 

它为我在Ubuntu12.04克++编译:

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

相关,以使定制操纵是无耻地从here但那儿剽窃不要怪他们我的无能copypasta位。

+3

这是一个错误在这里不使用RAII。 –

+0

恕我直言,一个无锁FIFO将是一个更好的主意在这里..以某种方式线程暂停记录不吸引力。 – vrdhn

+2

依靠用户锁定和解锁东西是不可靠的。一种选择是让您的日志文件同时从用户那里获取消息,并通过将它们放入单个队列并在单独的线程中运行它们来序列化它们。所以从多用户的角度来看,这些呼叫是非阻塞的,但实际上没有交织。参见[Herb Sutter的这篇演讲](http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Herb-Sutter-Concurrency-and-Parallelism)了解更多信息。我经历了实现他的并发对象包装的工作版本的练习。 – juanchopanza

回答

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(); 

这是完全没有额外的锁。

+0

啊哈!是的,我错过了缺乏安全性的例外,谢谢! – umlimo

+1

提及异常相关问题+1! – thokra

+0

关于接受哪个答案的艰难决定,但即使Useless的答案提供了一个很好的解决方案,但这个答案更直接。为所有人欢呼。 – umlimo

3

你并不需要显式地经过锁定机械手,你可以使用一个哨兵(带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. 创建一个临时哨兵对象
    • 其锁定日志对象
  2. ...转发每个operator<<呼叫到母体日志实例...
  3. 然后超出范围在表达式的结尾(或如果foo抛出)
    • 其解锁日志对象

Althou这是安全的,它也引发了很多争论(互斥锁被锁定的时间比我通常喜欢的要长,同时格式化消息)。较低争用的方法是根据本地存储(本地线程或本地范围)进行格式化,然后保持足够长的时间以将其移入共享日志记录队列。

+1

这该死的,那是优雅的...... –

+0

啊,我完全误解了Hans Passant对RAII的提及,但是这澄清了它,这是一个美丽的解决方案,谢谢!为什么我找不到这个优雅的方法? :D – umlimo

+0

恐怕我不记得我第一次看到这个想法的地方。顺便说一句,你可以通过给Sentry一个std :: unique_lock成员来清理它,而不是直接调用锁定和解锁(然后你可以免费移动ctor和dtor)。 – Useless

2

这不是一个非常好的主意,因为有人会致命 忘记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,它返回 这个类的一个实例,用一个有效的LogData (动态分配),或一个空指针,这取决于 测井是活动的还是不。 (这是可能避免的动态 分配,使用LogData具有 功能,启动日志记录,并结束它的静态实例,但它是 有点复杂。)

+0

这有点酷,但是真的有一个好处,就是'stringstream'消息,然后调用底层'ostream'上的'operator <<'?另外,'unique_lock'的移动锁定了互斥锁吗?否则,我认为你实际上并没有获得锁定,对吧?或者你传入一个已经锁定的'unique_lock'?这里一点困惑。 – thokra

+0

@thokra与使用ostringstream相比,有两个重要的好处:第一个是如果日志记录不活跃,则不会发生格式化;第二个是我实际使用特殊的'streambuf',它有特殊的功能来注入代码在每行的开始,并确保刷新的序列以“\ n”结尾。我也使用一个宏来调用'log',所以我可以自动传递'__FILE__'和'__LINE__'。而且我经常使用同一个streambuf对象,所以它的缓冲区很快达到了最大值,并且没有更多的分配。 –

+0

该锁在函数'log'中获得,该函数返回'Log',并且在构建时必须将其移动到'LogData'中。 (在我最初的代码中,'log'函数在构建'LogData'之前有几件事要做,而且我当时没有线程本地存储,所以可以选择在线程本地存储中使用streambuf ,并且只在冲洗期间锁定,对我来说不可用。 –