从我读到的standard output streams are generally not thread safe开始。我有一个C ++应用程序(基于Windows,使用Visual Studio 2005),它具有非常简单的日志记录功能:
void logText(string text)
{
if(g_OutputLogEnabled && g_OutputLog.is_open())
{
string logDate = getDateStamp("%Y-%m-%d %H:%M:%S");
g_OutputLog << "[" << logDate << "]: " << text << endl;
}
cout << text << endl; // Also echo on stdout
}
在此示例中,g_OutputLog
是一个流,g_OutputLogEnabled
是一个布尔值。
我一直在我的主应用程序中使用这个小函数没有问题,但我现在想将其用途扩展到一些子线程。这些线程在工作完成后可以正常工作和异步打印数据。
问题:如何在此例程中添加简单的行级线程安全性?所有我真正关心的是,我的日志中的每一行都保持不变。在这种情况下,性能不是问题,但(一如既往)更快更好。
我知道我可以使用第三方日志包,但我想自己做,所以我可以了解它是如何工作的。我的多线程知识不应该是它应该是什么,我正在努力改进它。
我听过critical sections这个词,我有点意识到互斥锁和信号量,但在这种情况下我会使用哪个?有一个干净,简单的解决方案吗?提前感谢任何建议。
答案 0 :(得分:6)
使用范围锁定,例如:
void logText(string text)
{
if(g_OutputLogEnabled && g_OutputLog.is_open())
{
string logDate = getDateStamp("%Y-%m-%d %H:%M:%S");
boost::scoped_lock (g_log_mutex); //lock
g_OutputLog << "[" << logDate << "]: " << text << endl;
} //mutex is released automatically here
boost::scoped_lock (g_cout_log_mutex); //lock on different mutex!
cout << text << endl; // Also echo on stdout
}
如果你的编译器支持,你可以使用std::unique_lock
。
scoped_lock
,你将如何实施std::unique_lock
?首先定义mutex
类:
#include <Windows.h>
class mutex : private CRITICAL_SECTION //inherit privately!
{
public:
mutex()
{
::InitializeCriticalSection(this);
}
~mutex()
{
::DeleteCriticalSection(this);
}
private:
friend class scoped_lock; //make scoped_lock a friend of mutex
//disable copy-semantic
mutex(mutex const &); //do not define it!
void operator=(mutex const &); //do not define it!
void lock()
{
::EnterCriticalSection(this);
}
void unlock()
{
::LeaveCriticalSection(this);
}
};
然后将scoped_lock
定义为:
class scoped_lock
{
mutex & m_mutex;
public:
scoped_lock(mutex & m) : m_mutex(m)
{
m_mutex.lock();
}
~scoped_lock()
{
m_mutex.unlock();
}
};
现在你可以使用它们了。
答案 1 :(得分:0)
鉴于该函数显然不是面向性能的,在写入流之前添加一个简单的互斥锁并锁定。
当然出于安全原因,应自动释放锁定,因此请务必使用RAII。
我建议你看一下Boost.Threads库。
答案 2 :(得分:0)
是的,您应该为代码添加某种保护。你不需要任何异国情调,你想要访问一个资源(你的流),而它可能正在被其他东西使用。两种类型的同步对象在此任务中表现良好:关键部分和互斥锁。有关锁定的更多详细信息,您可以开始阅读Wikipedia上的这篇文章。 Mutex通常较慢,但可以跨进程共享,这不是您的情况,因此您可以使用简单关键部分来同步您的线程。
如果你不打算使用第三方库(比如伟大的Boost),很少有提示。
当EnterCriticalSection函数获取锁定时。如果资源被其他人锁定,则当资源将由其所有者释放时,您的线程将被暂停并重新激活(此外,您的锁定线程可能提升其优先级)。 对于短生存锁,这可能不是最佳解决方案因为暂停/恢复线程耗费时间和资源。因此,您可以设置 spin ;在暂停你的线程之前,操作系统将花费一点时间在该线程上什么也不做,它可能会给锁定所有者完成其工作并释放thred的时间。要使用此功能,您必须使用InitializeCriticalSectionAndSpinCount而不是InitializeCriticalSection初始化关键部分。
如果您打算使用Critical Section,您可以考虑包装类中所需的所有内容,您将使用变量范围来执行所有操作,并且您的代码将更加清晰(这只是一个示例,真正的实现不能是太天真了):
class critical_section
{
public:
critical_section()
{
// Here you may use InitializeCriticalSectionAndSpinCount
InitializeCriticalSection(&_cs);
// You may not need this behavior, anyway here when you create
// the object you acquire the lock too
EnterCriticalSection(&_cs);
}
~critical_section()
{
LeaveCriticalSection(&_cs);
DeleteCriticalSection(&cs);
}
private:
CRITICAL_SECTION _cs;
};
在Unix / Linux下(或者你想要可移植),你应该使用Mutex的pthread.h
函数。
这只是第一步,如果您的应用程序记录很多,您可能会减慢等待日志的所有线程的速度(不做任何先验,检查和配置文件)。如果是这种情况,您应该创建一个队列,您的logText()
函数只需在队列中推送一个新的(预先格式化的)项目并调用PulseEvent来表示事件(使用CreateEvent创建的事件)。你将有第二个线程用WaitForSingleObject等待该事件,你的线程将被唤醒并且它将从队列中弹出一个项目。您可能仍然需要一些锁定机制(或者您可以编写自己的non-blocking concurrent queue以避免任何锁定。此解决方案更快,并且它不使用任何类型的锁,但我认为您应该做这样的事情只有你想要研究主题(线程),通常是为了简单的日志要求,你不需要添加这样的复杂性。