我一直在研究解决线程安全日志问题的各种方法,但我还没有看到类似的情况,所以我不知道是否因为我是C++、线程和iostreams的完全新手而没有注意到这一点。它似乎在我已经通过的基本测试中起作用。
基本上我有一个日志类(创意,我知道.)这是operator<<为标准操纵者设置的,这样我就可以随意地通过。
然而,我意识到这样的情况:
std::cout << "Threads" << " will" << " mess" << " with" << "this." << std::endl;
当多个线程写入cout (或日志流所指向的任何位置)时,可能会被交织。因此,我创建了一些特定于Log类的操作器,允许我这样做:
Log::log << lock << "Write" << " what" << " I" << " want" << std::endl << 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;
}
它在Ubuntu12.04 g++上为我工作,编译时:
g++ LogThreadTest.cpp -o log -std=c++0x -lpthread
与制造定制操纵器相关的部分是从这里中无耻地抄袭出来的,但不要因为我的不称职的复制体而责怪他们。
发布于 2013-10-22 14:10:06
这是个坏主意。想象一下:
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();
完全没有额外的锁。
发布于 2013-10-22 14:16:46
您不需要显式地传递锁机械手,您可以使用哨兵(如Hans所说,带有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;
威尔:
operator<<
调用转发给父日志实例..。foo
抛出)
虽然这是安全的,但它也会造成很多争用(在格式化消息时,互斥锁的时间比我通常希望的长)。一种低争用的方法是在没有锁定的情况下将格式设置到本地存储(线程-本地或作用域-本地),然后保持锁足够长的时间将其移动到共享日志队列中。
发布于 2013-10-22 14:38:09
这并不是一个好主意,因为总有一天有人会忘记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的单个指针成员可变,并将相同的代码放入具有传统签名的复制构造函数中。丑陋,但作为一个工作.)
在这个解决方案中,您将有一个函数log
,它返回这个类的一个实例,其中包含一个有效的LogData
(动态分配),或者一个空指针,这取决于日志记录是否是活动的。(可以通过使用LogData
的静态实例来避免动态分配,该实例具有启动日志记录和结束日志记录的功能,但它要复杂一些。)
https://stackoverflow.com/questions/19519590
复制相似问题