【发布时间】:2013-10-22 13:40:48
【问题描述】:
我一直在寻找解决线程安全日志记录问题的各种方法,但我还没有看到类似的东西,所以我不知道这是否有点糟糕,因为我完全是新手,所以我没有注意到C++、线程和 iostream。它似乎在我通过的基本测试中有效。
基本上我有一个 Log 类(有创意,我知道...),它为标准操纵器设置了 operator
但是,我知道类似:
std::cout << "Threads" << " will" << " mess" << " with" << "this." << std::endl;
当多个线程写入 cout(或 Log ostream 指向的任何位置)时,可能会发生交错。因此,我创建了一些特定于 Log 类的操纵器,让我可以这样做:
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;
}
它适用于我在 Ubuntu 12.04 g++ 上,编译为:
g++ LogThreadTest.cpp -o log -std=c++0x -lpthread
与制作自定义操纵器相关的部分被无耻地抄袭自here,但不要因为我的无能copypasta而责备他们。
【问题讨论】:
-
这里不使用RAII是错误的。
-
恕我直言,无锁 FIFO 在这里会是一个更好的主意。不知何故线程暂停日志记录并不吸引人。
-
依靠用户锁定和解锁某些东西是不可靠的。一种选择是让您的日志文件同时从用户那里获取消息,并通过将它们放在单个队列中并在单独的线程中运行它们来序列化它们。所以从多用户的角度来看,通话是非阻塞的,但实际上没有交错。有关更多信息,请参阅this talk by Herb Sutter。我完成了实现他的并发对象包装器的工作版本的练习。
-
非常正确:RAII;我应该在销毁时关闭 fstream,是吗?我在削减示例时将其删除,但删除 fstream 会破坏 ostream,因此我将其放回,但没有将 fstream.close() 放入析构函数中...我不知道是否要对 ostream 做任何事情关于销毁 - 如果 Log 实例是用 cerr/cout 构造的,我不想弄乱它,并且如果从 fstream 派生(如果 Log 是用文件名字符串或 fstream 构造的,那些构造函数未在此处显示)它将在关闭时处理,对吗?
-
“依赖用户锁定和解锁某些东西是不可靠的。”正如我所说,我愿意接受这一点 - 但同样,对于在编译时产生此错误的任何建议将不胜感激。
标签: c++ logging thread-safety iostream manipulators